diff options
492 files changed, 14902 insertions, 3634 deletions
diff --git a/Android.bp b/Android.bp index 9cb3067096cc..48f0928f24d7 100644 --- a/Android.bp +++ b/Android.bp @@ -399,6 +399,7 @@ java_defaults { "com.android.sysprop.foldlockbehavior", "com.android.sysprop.view", "framework-internal-utils", + "dynamic_instrumentation_manager_aidl-java", // If MimeMap ever becomes its own APEX, then this dependency would need to be removed // in favor of an API stubs dependency in java_library "framework" below. "mimemap", diff --git a/api/Android.bp b/api/Android.bp index 0ac85e28de1a..cdc5cd120956 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -127,27 +127,54 @@ combined_apis { }), } +// Create a single file containing the latest released version of the whole +// Android public API. +java_genrule { + name: "android.api.merged.public.latest", + srcs: [ + ":android.api.combined.public.latest", + ], + out: ["public-latest.txt"], + tools: ["metalava"], + cmd: metalava_cmd + " merge-signatures --format=2.0 $(in) --out $(out)", +} + +// Make sure that the Android public API is compatible with the +// previously released public API. java_genrule { name: "frameworks-base-api-current-compat", srcs: [ - ":android.api.public.latest", + ":android.api.merged.public.latest", ":android-incompatibilities.api.public.latest", ":frameworks-base-api-current.txt", ], out: ["updated-baseline.txt"], tools: ["metalava"], cmd: metalava_cmd + - "--check-compatibility:api:released $(location :android.api.public.latest) " + + "--check-compatibility:api:released $(location :android.api.merged.public.latest) " + "--baseline:compatibility:released $(location :android-incompatibilities.api.public.latest) " + "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " + "$(location :frameworks-base-api-current.txt)", } +// Create a single file containing the latest released version of the whole +// Android system API. +java_genrule { + name: "android.api.merged.system.latest", + srcs: [ + ":android.api.combined.system.latest", + ], + out: ["system-latest.txt"], + tools: ["metalava"], + cmd: metalava_cmd + " merge-signatures --format=2.0 $(in) --out $(out)", +} + +// Make sure that the Android system API is compatible with the +// previously released system API. java_genrule { name: "frameworks-base-api-system-current-compat", srcs: [ - ":android.api.public.latest", - ":android.api.system.latest", + ":android.api.merged.system.latest", ":android-incompatibilities.api.system.latest", ":frameworks-base-api-current.txt", ":frameworks-base-api-system-current.txt", @@ -155,20 +182,31 @@ java_genrule { out: ["updated-baseline.txt"], tools: ["metalava"], cmd: metalava_cmd + - "--check-compatibility:api:released $(location :android.api.public.latest) " + - "--check-compatibility:api:released $(location :android.api.system.latest) " + + "--check-compatibility:api:released $(location :android.api.merged.system.latest) " + "--baseline:compatibility:released $(location :android-incompatibilities.api.system.latest) " + "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " + "$(location :frameworks-base-api-current.txt) " + "$(location :frameworks-base-api-system-current.txt)", } +// Create a single file containing the latest released version of the whole +// Android module-lib API. +java_genrule { + name: "android.api.merged.module-lib.latest", + srcs: [ + ":android.api.combined.module-lib.latest", + ], + out: ["module-lib-latest.txt"], + tools: ["metalava"], + cmd: metalava_cmd + " merge-signatures --format=2.0 $(in) --out $(out)", +} + +// Make sure that the Android module-lib API is compatible with the +// previously released module-lib API. java_genrule { name: "frameworks-base-api-module-lib-current-compat", srcs: [ - ":android.api.public.latest", - ":android.api.system.latest", - ":android.api.module-lib.latest", + ":android.api.merged.module-lib.latest", ":android-incompatibilities.api.module-lib.latest", ":frameworks-base-api-current.txt", ":frameworks-base-api-system-current.txt", @@ -177,9 +215,7 @@ java_genrule { out: ["updated-baseline.txt"], tools: ["metalava"], cmd: metalava_cmd + - "--check-compatibility:api:released $(location :android.api.public.latest) " + - "--check-compatibility:api:released $(location :android.api.system.latest) " + - "--check-compatibility:api:released $(location :android.api.module-lib.latest) " + + "--check-compatibility:api:released $(location :android.api.merged.module-lib.latest) " + "--baseline:compatibility:released $(location :android-incompatibilities.api.module-lib.latest) " + "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " + "$(location :frameworks-base-api-current.txt) " + @@ -194,7 +230,7 @@ java_genrule { cmd: "$(location merge_zips) $(out) $(in)", srcs: [ ":api-stubs-docs-non-updatable{.exportable}", - ":all-modules-public-stubs-source", + ":all-modules-public-stubs-source-exportable", ], visibility: ["//visibility:private"], // Used by make module in //development, mind } diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp index e8fcf4b2b32d..1ebe0cdfabd7 100644 --- a/api/ApiDocs.bp +++ b/api/ApiDocs.bp @@ -129,7 +129,7 @@ droidstubs { droidstubs { name: "framework-doc-stubs", defaults: ["android-non-updatable-doc-stubs-defaults"], - srcs: [":all-modules-public-stubs-source"], + srcs: [":all-modules-public-stubs-source-exportable"], api_levels_module: "api_versions_public", aidl: { include_dirs: [ diff --git a/api/api.go b/api/api.go index f32bdc32f75d..5ca24de1b46a 100644 --- a/api/api.go +++ b/api/api.go @@ -429,8 +429,9 @@ func createMergedFrameworkSystemServerExportableStubs(ctx android.LoadHookContex func createPublicStubsSourceFilegroup(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { props := fgProps{} - props.Name = proptools.StringPtr("all-modules-public-stubs-source") - props.Device_common_srcs = createSrcs(modules, "{.public.stubs.source}") + props.Name = proptools.StringPtr("all-modules-public-stubs-source-exportable") + transformConfigurableArray(modules, "", ".stubs.source") + props.Device_common_srcs = createSrcs(modules, "{.exportable}") props.Visibility = []string{"//frameworks/base"} ctx.CreateModule(android.FileGroupFactory, &props) } diff --git a/cmds/idmap2/libidmap2/ResourceContainer.cpp b/cmds/idmap2/libidmap2/ResourceContainer.cpp index 3c0e118bbfe7..57ae3548123b 100644 --- a/cmds/idmap2/libidmap2/ResourceContainer.cpp +++ b/cmds/idmap2/libidmap2/ResourceContainer.cpp @@ -17,6 +17,7 @@ #include "idmap2/ResourceContainer.h" #include <memory> +#include <mutex> #include <string> #include <utility> #include <vector> @@ -296,7 +297,7 @@ struct ResState { } // namespace struct ApkResourceContainer : public TargetResourceContainer, public OverlayResourceContainer { - static Result<std::unique_ptr<ApkResourceContainer>> FromPath(const std::string& path); + static Result<std::unique_ptr<ApkResourceContainer>> FromPath(std::string path); // inherited from TargetResourceContainer Result<bool> DefinesOverlayable() const override; @@ -320,6 +321,7 @@ struct ApkResourceContainer : public TargetResourceContainer, public OverlayReso Result<const ResState*> GetState() const; ZipAssetsProvider* GetZipAssets() const; + mutable std::mutex state_lock_; mutable std::variant<std::unique_ptr<ZipAssetsProvider>, ResState> state_; std::string path_; }; @@ -330,16 +332,17 @@ ApkResourceContainer::ApkResourceContainer(std::unique_ptr<ZipAssetsProvider> zi } Result<std::unique_ptr<ApkResourceContainer>> ApkResourceContainer::FromPath( - const std::string& path) { + std::string path) { auto zip_assets = ZipAssetsProvider::Create(path, 0 /* flags */); if (zip_assets == nullptr) { return Error("failed to load zip assets"); } return std::unique_ptr<ApkResourceContainer>( - new ApkResourceContainer(std::move(zip_assets), path)); + new ApkResourceContainer(std::move(zip_assets), std::move(path))); } Result<const ResState*> ApkResourceContainer::GetState() const { + std::lock_guard lock(state_lock_); if (auto state = std::get_if<ResState>(&state_); state != nullptr) { return state; } @@ -355,6 +358,7 @@ Result<const ResState*> ApkResourceContainer::GetState() const { } ZipAssetsProvider* ApkResourceContainer::GetZipAssets() const { + std::lock_guard lock(state_lock_); if (auto zip = std::get_if<std::unique_ptr<ZipAssetsProvider>>(&state_); zip != nullptr) { return zip->get(); } @@ -427,7 +431,7 @@ Result<std::string> ApkResourceContainer::GetResourceName(ResourceId id) const { Result<std::unique_ptr<TargetResourceContainer>> TargetResourceContainer::FromPath( std::string path) { - auto result = ApkResourceContainer::FromPath(path); + auto result = ApkResourceContainer::FromPath(std::move(path)); if (!result) { return result.GetError(); } @@ -438,7 +442,7 @@ Result<std::unique_ptr<OverlayResourceContainer>> OverlayResourceContainer::From std::string path) { // Load the path as a fabricated overlay if the file magic indicates this is a fabricated overlay. if (android::IsFabricatedOverlay(path)) { - auto result = FabricatedOverlayContainer::FromPath(path); + auto result = FabricatedOverlayContainer::FromPath(std::move(path)); if (!result) { return result.GetError(); } @@ -446,7 +450,7 @@ Result<std::unique_ptr<OverlayResourceContainer>> OverlayResourceContainer::From } // Fallback to loading the container as an APK. - auto result = ApkResourceContainer::FromPath(path); + auto result = ApkResourceContainer::FromPath(std::move(path)); if (!result) { return result.GetError(); } diff --git a/core/api/current.txt b/core/api/current.txt index eb4973999e25..fda66fa65e7e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -314,6 +314,7 @@ package android { field public static final String SYSTEM_ALERT_WINDOW = "android.permission.SYSTEM_ALERT_WINDOW"; field public static final String TRANSMIT_IR = "android.permission.TRANSMIT_IR"; field public static final String TURN_SCREEN_ON = "android.permission.TURN_SCREEN_ON"; + field @FlaggedApi("android.app.enable_tv_implicit_enter_pip_restriction") public static final String TV_IMPLICIT_ENTER_PIP = "android.permission.TV_IMPLICIT_ENTER_PIP"; field public static final String UNINSTALL_SHORTCUT = "com.android.launcher.permission.UNINSTALL_SHORTCUT"; field public static final String UPDATE_DEVICE_STATS = "android.permission.UPDATE_DEVICE_STATS"; field public static final String UPDATE_PACKAGES_WITHOUT_USER_ACTION = "android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"; @@ -477,6 +478,8 @@ package android { field public static final int alpha = 16843551; // 0x101031f field public static final int alphabeticModifiers = 16844110; // 0x101054e field public static final int alphabeticShortcut = 16843235; // 0x10101e3 + field @FlaggedApi("android.content.pm.change_launcher_badging") public static final int alternateLauncherIcons; + field @FlaggedApi("android.content.pm.change_launcher_badging") public static final int alternateLauncherLabels; field public static final int alwaysDrawnWithCache = 16842991; // 0x10100ef field public static final int alwaysRetainTaskState = 16843267; // 0x1010203 field @Deprecated public static final int amPmBackgroundColor = 16843941; // 0x10104a5 @@ -8789,8 +8792,31 @@ package android.app.admin { package android.app.appfunctions { + @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionException extends java.lang.Exception implements android.os.Parcelable { + ctor public AppFunctionException(int, @Nullable String); + ctor public AppFunctionException(int, @Nullable String, @NonNull android.os.Bundle); + method public int describeContents(); + method public int getErrorCategory(); + method public int getErrorCode(); + method @Nullable public String getErrorMessage(); + method @NonNull public android.os.Bundle getExtras(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.appfunctions.AppFunctionException> CREATOR; + field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8 + field public static final int ERROR_CANCELLED = 2001; // 0x7d1 + field public static final int ERROR_CATEGORY_APP = 3; // 0x3 + field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 + field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 + field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 + field public static final int ERROR_DENIED = 1000; // 0x3e8 + field public static final int ERROR_DISABLED = 1002; // 0x3ea + field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb + field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9 + field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0 + } + @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager { - method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); @@ -8802,7 +8828,7 @@ package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } @@ -8824,30 +8850,14 @@ package android.app.appfunctions { } @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class ExecuteAppFunctionResponse implements android.os.Parcelable { + ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument); + ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument, @NonNull android.os.Bundle); method public int describeContents(); - method public int getErrorCategory(); - method @Nullable public String getErrorMessage(); method @NonNull public android.os.Bundle getExtras(); - method public int getResultCode(); method @NonNull public android.app.appsearch.GenericDocument getResultDocument(); - method public boolean isSuccess(); - method @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") @NonNull public static android.app.appfunctions.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle); - method @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") @NonNull public static android.app.appfunctions.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.appfunctions.ExecuteAppFunctionResponse> CREATOR; - field public static final int ERROR_CATEGORY_APP = 3; // 0x3 - field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 - field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 - field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 - field public static final String PROPERTY_RETURN_VALUE = "returnValue"; - field public static final int RESULT_APP_UNKNOWN_ERROR = 3000; // 0xbb8 - field public static final int RESULT_CANCELLED = 2001; // 0x7d1 - field public static final int RESULT_DENIED = 1000; // 0x3e8 - field public static final int RESULT_DISABLED = 1002; // 0x3ea - field public static final int RESULT_FUNCTION_NOT_FOUND = 1003; // 0x3eb - field public static final int RESULT_INVALID_ARGUMENT = 1001; // 0x3e9 - field public static final int RESULT_OK = 0; // 0x0 - field public static final int RESULT_SYSTEM_ERROR = 2000; // 0x7d0 + field public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue"; } } @@ -8870,6 +8880,7 @@ package android.app.assist { method public void setWebUri(android.net.Uri); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.assist.AssistContent> CREATOR; + field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXTRA_APP_FUNCTION_DATA = "android.app.assist.extra.APP_FUNCTION_DATA"; } public class AssistStructure implements android.os.Parcelable { @@ -19772,6 +19783,8 @@ package android.hardware.camera2 { field public static final int CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0; // 0x0 field public static final int CONTROL_VIDEO_STABILIZATION_MODE_ON = 1; // 0x1 field public static final int CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION = 2; // 0x2 + field @FlaggedApi("com.android.internal.camera.flags.zoom_method") public static final int CONTROL_ZOOM_METHOD_AUTO = 0; // 0x0 + field @FlaggedApi("com.android.internal.camera.flags.zoom_method") public static final int CONTROL_ZOOM_METHOD_ZOOM_RATIO = 1; // 0x1 field public static final int DISTORTION_CORRECTION_MODE_FAST = 1; // 0x1 field public static final int DISTORTION_CORRECTION_MODE_HIGH_QUALITY = 2; // 0x2 field public static final int DISTORTION_CORRECTION_MODE_OFF = 0; // 0x0 @@ -19979,6 +19992,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SCENE_MODE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE; + field @FlaggedApi("com.android.internal.camera.flags.zoom_method") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_ZOOM_METHOD; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Float> CONTROL_ZOOM_RATIO; field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.CaptureRequest> CREATOR; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE; @@ -20077,6 +20091,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SCENE_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE; + field @FlaggedApi("com.android.internal.camera.flags.zoom_method") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_ZOOM_METHOD; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Float> CONTROL_ZOOM_RATIO; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EDGE_MODE; @@ -21001,6 +21016,7 @@ package android.inputmethodservice { method @Deprecated public android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface(); method public android.view.View onCreateInputView(); method protected void onCurrentInputMethodSubtypeChanged(android.view.inputmethod.InputMethodSubtype); + method @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public void onCustomImeSwitcherButtonRequestedVisible(boolean); method public void onDisplayCompletions(android.view.inputmethod.CompletionInfo[]); method public boolean onEvaluateFullscreenMode(); method @CallSuper public boolean onEvaluateInputViewShown(); @@ -22888,6 +22904,7 @@ package android.media { method public void onCryptoError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CryptoException); method public abstract void onError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CodecException); method public abstract void onInputBufferAvailable(@NonNull android.media.MediaCodec, int); + method @FlaggedApi("android.media.codec.subsession_metrics") public void onMetricsFlushed(@NonNull android.media.MediaCodec, @NonNull android.os.PersistableBundle); method public abstract void onOutputBufferAvailable(@NonNull android.media.MediaCodec, int, @NonNull android.media.MediaCodec.BufferInfo); method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void onOutputBuffersAvailable(@NonNull android.media.MediaCodec, int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>); method public abstract void onOutputFormatChanged(@NonNull android.media.MediaCodec, @NonNull android.media.MediaFormat); @@ -42148,6 +42165,25 @@ package android.service.settings.preferences { method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWriteSensitivity(int); } + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public abstract class SettingsPreferenceService extends android.app.Service { + ctor public SettingsPreferenceService(); + method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); + method public abstract void onGetAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>); + method public abstract void onGetPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>); + method public abstract void onSetPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>); + field public static final String ACTION_PREFERENCE_SERVICE = "android.service.settings.preferences.action.PREFERENCE_SERVICE"; + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable { + ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String); + method public void close(); + method public void getAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>); + method public void getPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>); + method public void setPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>); + method public void start(); + method public void stop(); + } + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceValue implements android.os.Parcelable { method public int describeContents(); method public boolean getBooleanValue(); @@ -44279,6 +44315,8 @@ package android.telephony { field public static final String ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED"; field public static final int CARRIER_NR_AVAILABILITY_NSA = 1; // 0x1 field public static final int CARRIER_NR_AVAILABILITY_SA = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int CARRIER_ROAMING_NTN_CONNECT_AUTOMATIC = 0; // 0x0 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int CARRIER_ROAMING_NTN_CONNECT_MANUAL = 1; // 0x1 field public static final int CROSS_SIM_SPN_FORMAT_CARRIER_NAME_ONLY = 0; // 0x0 field public static final int CROSS_SIM_SPN_FORMAT_CARRIER_NAME_WITH_BRANDING = 1; // 0x1 field public static final int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe @@ -44349,11 +44387,14 @@ package android.telephony { field public static final String KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY = "carrier_nr_availabilities_int_array"; field public static final String KEY_CARRIER_PROVISIONS_WIFI_MERGED_NETWORKS_BOOL = "carrier_provisions_wifi_merged_networks_bool"; field public static final String KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL = "carrier_rcs_provisioning_required_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT = "carrier_roaming_ntn_connect_type_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_CARRIER_ROAMING_NTN_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_INT = "carrier_roaming_ntn_emergency_call_to_satellite_handover_type_int"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_CARRIER_ROAMING_SATELLITE_DEFAULT_SERVICES_INT_ARRAY = "carrier_roaming_satellite_default_services_int_array"; field public static final String KEY_CARRIER_SERVICE_NAME_STRING_ARRAY = "carrier_service_name_array"; field public static final String KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY = "carrier_service_number_array"; field public static final String KEY_CARRIER_SETTINGS_ACTIVITY_COMPONENT_NAME_STRING = "carrier_settings_activity_component_name_string"; field public static final String KEY_CARRIER_SETTINGS_ENABLE_BOOL = "carrier_settings_enable_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_CARRIER_SUPPORTED_SATELLITE_NOTIFICATION_HYSTERESIS_SEC_INT = "carrier_supported_satellite_notification_hysteresis_sec_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE = "carrier_supported_satellite_services_per_provider_bundle"; field public static final String KEY_CARRIER_SUPPORTS_OPP_DATA_AUTO_PROVISIONING_BOOL = "carrier_supports_opp_data_auto_provisioning_bool"; field public static final String KEY_CARRIER_SUPPORTS_SS_OVER_UT_BOOL = "carrier_supports_ss_over_ut_bool"; @@ -44404,6 +44445,7 @@ package android.telephony { field public static final String KEY_DIAL_STRING_REPLACE_STRING_ARRAY = "dial_string_replace_string_array"; field public static final String KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL = "disable_cdma_activation_code_bool"; field public static final String KEY_DISABLE_CHARGE_INDICATION_BOOL = "disable_charge_indication_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL = "disable_dun_apn_while_roaming_with_preset_apn_bool"; field public static final String KEY_DISABLE_SUPPLEMENTARY_SERVICES_IN_AIRPLANE_MODE_BOOL = "disable_supplementary_services_in_airplane_mode_bool"; field public static final String KEY_DISCONNECT_CAUSE_PLAY_BUSYTONE_INT_ARRAY = "disconnect_cause_play_busytone_int_array"; field public static final String KEY_DISPLAY_CALL_STRENGTH_INDICATOR_BOOL = "display_call_strength_indicator_bool"; @@ -44416,6 +44458,8 @@ package android.telephony { field public static final String KEY_EDITABLE_VOICEMAIL_NUMBER_SETTING_BOOL = "editable_voicemail_number_setting_bool"; field public static final String KEY_EDITABLE_WFC_MODE_BOOL = "editable_wfc_mode_bool"; field public static final String KEY_EDITABLE_WFC_ROAMING_MODE_BOOL = "editable_wfc_roaming_mode_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT = "emergency_call_to_satellite_t911_handover_timeout_millis_int"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL = "emergency_messaging_supported_bool"; field public static final String KEY_EMERGENCY_NOTIFICATION_DELAY_INT = "emergency_notification_delay_int"; field public static final String KEY_EMERGENCY_NUMBER_PREFIX_STRING_ARRAY = "emergency_number_prefix_string_array"; field public static final String KEY_ENABLE_CROSS_SIM_CALLING_ON_OPPORTUNISTIC_DATA_BOOL = "enable_cross_sim_calling_on_opportunistic_data_bool"; @@ -44462,6 +44506,7 @@ package android.telephony { field public static final String KEY_MMS_MAX_IMAGE_HEIGHT_INT = "maxImageHeight"; field public static final String KEY_MMS_MAX_IMAGE_WIDTH_INT = "maxImageWidth"; field public static final String KEY_MMS_MAX_MESSAGE_SIZE_INT = "maxMessageSize"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_MMS_MAX_NTN_PAYLOAD_SIZE_BYTES_INT = "mms_max_ntn_payload_size_bytes_int"; field public static final String KEY_MMS_MESSAGE_TEXT_MAX_SIZE_INT = "maxMessageTextSize"; field public static final String KEY_MMS_MMS_DELIVERY_REPORT_ENABLED_BOOL = "enableMMSDeliveryReports"; field public static final String KEY_MMS_MMS_ENABLED_BOOL = "enabledMMS"; @@ -44500,6 +44545,7 @@ package android.telephony { field public static final String KEY_OPPORTUNISTIC_NETWORK_EXIT_THRESHOLD_RSSNR_INT = "opportunistic_network_exit_threshold_rssnr_int"; field public static final String KEY_OPPORTUNISTIC_NETWORK_MAX_BACKOFF_TIME_LONG = "opportunistic_network_max_backoff_time_long"; field public static final String KEY_OPPORTUNISTIC_NETWORK_PING_PONG_TIME_LONG = "opportunistic_network_ping_pong_time_long"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_OVERRIDE_WFC_ROAMING_MODE_WHILE_USING_NTN_BOOL = "override_wfc_roaming_mode_while_using_ntn_bool"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT = "parameters_used_for_ntn_lte_signal_bar_int"; field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool"; field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool"; @@ -44518,6 +44564,7 @@ package android.telephony { field public static final String KEY_RCS_CONFIG_SERVER_URL_STRING = "rcs_config_server_url_string"; field public static final String KEY_READ_ONLY_APN_FIELDS_STRING_ARRAY = "read_only_apn_fields_string_array"; field public static final String KEY_READ_ONLY_APN_TYPES_STRING_ARRAY = "read_only_apn_types_string_array"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL = "remove_satellite_plmn_in_manual_network_scan_bool"; field public static final String KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL = "require_entitlement_checks_bool"; field @Deprecated public static final String KEY_RESTART_RADIO_ON_PDP_FAIL_REGULAR_DEACTIVATION_BOOL = "restart_radio_on_pdp_fail_regular_deactivation_bool"; field public static final String KEY_RTT_AUTO_UPGRADE_BOOL = "rtt_auto_upgrade_bool"; @@ -44529,13 +44576,18 @@ package android.telephony { field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DATA_SUPPORT_MODE_INT = "satellite_data_support_mode_int"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING = "satellite_entitlement_app_name_string"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = "satellite_entitlement_status_refresh_days_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = "satellite_entitlement_supported_bool"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ESOS_SUPPORTED_BOOL = "satellite_esos_supported_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING = "satellite_information_redirect_url_string"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_NIDD_APN_NAME_STRING = "satellite_nidd_apn_name_string"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_ESOS_INACTIVITY_TIMEOUT_SEC_INT = "satellite_roaming_esos_inactivity_timeout_sec_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT = "satellite_roaming_p2p_sms_inactivity_timeout_sec_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_P2P_SMS_SUPPORTED_BOOL = "satellite_roaming_p2p_sms_supported_bool"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT = "satellite_roaming_screen_off_inactivity_timeout_sec_int"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_ROAMING_TURN_OFF_SESSION_FOR_EMERGENCY_CALL_BOOL = "satellite_roaming_turn_off_session_for_emergency_call_bool"; field public static final String KEY_SHOW_4G_FOR_3G_DATA_ICON_BOOL = "show_4g_for_3g_data_icon_bool"; field public static final String KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL = "show_4g_for_lte_data_icon_bool"; field public static final String KEY_SHOW_APN_SETTING_CDMA_BOOL = "show_apn_setting_cdma_bool"; @@ -44605,6 +44657,9 @@ package android.telephony { field public static final String KEY_WORLD_MODE_ENABLED_BOOL = "world_mode_enabled_bool"; field public static final String KEY_WORLD_PHONE_BOOL = "world_phone_bool"; field public static final String REMOVE_GROUP_UUID_STRING = "00000000-0000-0000-0000-000000000000"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_DATA_SUPPORT_ALL = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_DATA_SUPPORT_BANDWIDTH_CONSTRAINED = 1; // 0x1 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED = 0; // 0x0 field public static final int SERVICE_CLASS_NONE = 0; // 0x0 field public static final int SERVICE_CLASS_VOICE = 1; // 0x1 field public static final int USSD_OVER_CS_ONLY = 2; // 0x2 @@ -51974,6 +52029,7 @@ package android.view { field public static final int KEYCODE_CHANNEL_DOWN = 167; // 0xa7 field public static final int KEYCODE_CHANNEL_UP = 166; // 0xa6 field public static final int KEYCODE_CLEAR = 28; // 0x1c + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_CLOSE = 321; // 0x141 field public static final int KEYCODE_COMMA = 55; // 0x37 field public static final int KEYCODE_CONTACTS = 207; // 0xcf field public static final int KEYCODE_COPY = 278; // 0x116 @@ -51986,6 +52042,8 @@ package android.view { field public static final int KEYCODE_DEMO_APP_2 = 302; // 0x12e field public static final int KEYCODE_DEMO_APP_3 = 303; // 0x12f field public static final int KEYCODE_DEMO_APP_4 = 304; // 0x130 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_DICTATE = 319; // 0x13f + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_DO_NOT_DISTURB = 322; // 0x142 field public static final int KEYCODE_DPAD_CENTER = 23; // 0x17 field public static final int KEYCODE_DPAD_DOWN = 20; // 0x14 field public static final int KEYCODE_DPAD_DOWN_LEFT = 269; // 0x10d @@ -51998,7 +52056,7 @@ package android.view { field public static final int KEYCODE_DVR = 173; // 0xad field public static final int KEYCODE_E = 33; // 0x21 field public static final int KEYCODE_EISU = 212; // 0xd4 - field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_EMOJI_PICKER = 317; // 0x13d + field public static final int KEYCODE_EMOJI_PICKER = 317; // 0x13d field public static final int KEYCODE_ENDCALL = 6; // 0x6 field public static final int KEYCODE_ENTER = 66; // 0x42 field public static final int KEYCODE_ENVELOPE = 65; // 0x41 @@ -52010,7 +52068,19 @@ package android.view { field public static final int KEYCODE_F10 = 140; // 0x8c field public static final int KEYCODE_F11 = 141; // 0x8d field public static final int KEYCODE_F12 = 142; // 0x8e + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F13 = 326; // 0x146 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F14 = 327; // 0x147 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F15 = 328; // 0x148 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F16 = 329; // 0x149 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F17 = 330; // 0x14a + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F18 = 331; // 0x14b + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F19 = 332; // 0x14c field public static final int KEYCODE_F2 = 132; // 0x84 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F20 = 333; // 0x14d + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F21 = 334; // 0x14e + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F22 = 335; // 0x14f + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F23 = 336; // 0x150 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F24 = 337; // 0x151 field public static final int KEYCODE_F3 = 133; // 0x85 field public static final int KEYCODE_F4 = 134; // 0x86 field public static final int KEYCODE_F5 = 135; // 0x87 @@ -52025,6 +52095,7 @@ package android.view { field public static final int KEYCODE_FOCUS = 80; // 0x50 field public static final int KEYCODE_FORWARD = 125; // 0x7d field public static final int KEYCODE_FORWARD_DEL = 112; // 0x70 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_FULLSCREEN = 325; // 0x145 field public static final int KEYCODE_FUNCTION = 119; // 0x77 field public static final int KEYCODE_G = 35; // 0x23 field public static final int KEYCODE_GRAVE = 68; // 0x44 @@ -52048,6 +52119,7 @@ package android.view { field public static final int KEYCODE_LANGUAGE_SWITCH = 204; // 0xcc field public static final int KEYCODE_LAST_CHANNEL = 229; // 0xe5 field public static final int KEYCODE_LEFT_BRACKET = 71; // 0x47 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_LOCK = 324; // 0x144 field public static final int KEYCODE_M = 41; // 0x29 field public static final int KEYCODE_MACRO_1 = 313; // 0x139 field public static final int KEYCODE_MACRO_2 = 314; // 0x13a @@ -52085,6 +52157,7 @@ package android.view { field public static final int KEYCODE_NAVIGATE_NEXT = 261; // 0x105 field public static final int KEYCODE_NAVIGATE_OUT = 263; // 0x107 field public static final int KEYCODE_NAVIGATE_PREVIOUS = 260; // 0x104 + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_NEW = 320; // 0x140 field public static final int KEYCODE_NOTIFICATION = 83; // 0x53 field public static final int KEYCODE_NUM = 78; // 0x4e field public static final int KEYCODE_NUMPAD_0 = 144; // 0x90 @@ -52119,6 +52192,7 @@ package android.view { field public static final int KEYCODE_PLUS = 81; // 0x51 field public static final int KEYCODE_POUND = 18; // 0x12 field public static final int KEYCODE_POWER = 26; // 0x1a + field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_PRINT = 323; // 0x143 field public static final int KEYCODE_PROFILE_SWITCH = 288; // 0x120 field public static final int KEYCODE_PROG_BLUE = 186; // 0xba field public static final int KEYCODE_PROG_GREEN = 184; // 0xb8 @@ -52131,7 +52205,7 @@ package android.view { field public static final int KEYCODE_RIGHT_BRACKET = 72; // 0x48 field public static final int KEYCODE_RO = 217; // 0xd9 field public static final int KEYCODE_S = 47; // 0x2f - field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_SCREENSHOT = 318; // 0x13e + field public static final int KEYCODE_SCREENSHOT = 318; // 0x13e field public static final int KEYCODE_SCROLL_LOCK = 116; // 0x74 field public static final int KEYCODE_SEARCH = 84; // 0x54 field public static final int KEYCODE_SEMICOLON = 74; // 0x4a @@ -55585,7 +55659,7 @@ package android.view.accessibility { method public android.os.Bundle getExtras(); method public CharSequence getHintText(); method public int getInputType(); - method public android.view.accessibility.AccessibilityNodeInfo getLabelFor(); + method @Deprecated @FlaggedApi("android.view.accessibility.deprecate_ani_label_for_apis") public android.view.accessibility.AccessibilityNodeInfo getLabelFor(); method @Deprecated @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public android.view.accessibility.AccessibilityNodeInfo getLabeledBy(); method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") @NonNull public java.util.List<android.view.accessibility.AccessibilityNodeInfo> getLabeledByList(); method public int getLiveRegion(); @@ -55684,8 +55758,8 @@ package android.view.accessibility { method public void setHintText(CharSequence); method public void setImportantForAccessibility(boolean); method public void setInputType(int); - method public void setLabelFor(android.view.View); - method public void setLabelFor(android.view.View, int); + method @Deprecated @FlaggedApi("android.view.accessibility.deprecate_ani_label_for_apis") public void setLabelFor(android.view.View); + method @Deprecated @FlaggedApi("android.view.accessibility.deprecate_ani_label_for_apis") public void setLabelFor(android.view.View, int); method @Deprecated @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void setLabeledBy(android.view.View); method @Deprecated @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void setLabeledBy(android.view.View, int); method public void setLiveRegion(int); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index a9b181d51fb4..70fbad01cae6 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -138,6 +138,7 @@ package android { field public static final String DISABLE_SYSTEM_SOUND_EFFECTS = "android.permission.DISABLE_SYSTEM_SOUND_EFFECTS"; field public static final String DISPATCH_PROVISIONING_MESSAGE = "android.permission.DISPATCH_PROVISIONING_MESSAGE"; field public static final String DOMAIN_VERIFICATION_AGENT = "android.permission.DOMAIN_VERIFICATION_AGENT"; + field @FlaggedApi("com.android.art.flags.executable_method_file_offsets") public static final String DYNAMIC_INSTRUMENTATION = "android.permission.DYNAMIC_INSTRUMENTATION"; field @FlaggedApi("com.android.window.flags.untrusted_embedding_any_app_permission") public static final String EMBED_ANY_APP_IN_UNTRUSTED_MODE = "android.permission.EMBED_ANY_APP_IN_UNTRUSTED_MODE"; field @FlaggedApi("android.content.pm.emergency_install_permission") public static final String EMERGENCY_INSTALL_PACKAGES = "android.permission.EMERGENCY_INSTALL_PACKAGES"; field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED"; @@ -706,6 +707,7 @@ package android.app { field public static final String OPSTR_READ_MEDIA_IMAGES = "android:read_media_images"; field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video"; field public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = "android:read_media_visual_user_selected"; + field @FlaggedApi("android.permission.flags.platform_oxygen_saturation_enabled") public static final String OPSTR_READ_OXYGEN_SATURATION = "android:read_oxygen_saturation"; field @FlaggedApi("android.permission.flags.platform_skin_temperature_enabled") public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature"; field public static final String OPSTR_READ_WRITE_HEALTH_DATA = "android:read_write_health_data"; field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio"; @@ -1268,13 +1270,21 @@ package android.app { public class WallpaperManager { method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public void clearWallpaper(int, int); + method @FlaggedApi("android.app.customization_packs_apis") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.util.SparseArray<android.graphics.Rect> getBitmapCrops(int); + method @FlaggedApi("android.app.customization_packs_apis") public static int getOrientation(@NonNull android.graphics.Point); method @FloatRange(from=0.0f, to=1.0f) @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public float getWallpaperDimAmount(); + method @FlaggedApi("android.app.customization_packs_apis") @Nullable public android.os.ParcelFileDescriptor getWallpaperFile(int, boolean); method @FlaggedApi("android.app.live_wallpaper_content_handling") @Nullable @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.app.wallpaper.WallpaperInstance getWallpaperInstance(int); method public void setDisplayOffset(android.os.IBinder, int, int); + method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setStreamWithCrops(@NonNull java.io.InputStream, @NonNull android.util.SparseArray<android.graphics.Rect>, boolean, int) throws java.io.IOException; method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) public boolean setWallpaperComponent(android.content.ComponentName); method @FlaggedApi("android.app.live_wallpaper_content_handling") @RequiresPermission(allOf={android.Manifest.permission.SET_WALLPAPER_COMPONENT, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public boolean setWallpaperComponentWithDescription(@NonNull android.app.wallpaper.WallpaperDescription, int); method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) public boolean setWallpaperComponentWithFlags(@NonNull android.content.ComponentName, int); method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public void setWallpaperDimAmount(@FloatRange(from=0.0f, to=1.0f) float); + field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_LANDSCAPE = 1; // 0x1 + field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_PORTRAIT = 0; // 0x0 + field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_SQUARE_LANDSCAPE = 3; // 0x3 + field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_SQUARE_PORTRAIT = 2; // 0x2 } } @@ -18331,7 +18341,12 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForModemStateChanged(@NonNull android.telephony.satellite.SatelliteModemStateCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForNtnSignalStrengthChanged(@NonNull android.telephony.satellite.NtnSignalStrengthCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForProvisionStateChanged(@NonNull android.telephony.satellite.SatelliteProvisionStateCallback); + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS = 7; // 0x7 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3; // 0x3 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5; // 0x5 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP = 4; // 0x4 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_SMS = 6; // 0x6 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_SOS_MESSAGE = 1; // 0x1 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_UNKNOWN = 0; // 0x0 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DEVICE_HOLD_POSITION_LANDSCAPE_LEFT = 2; // 0x2 @@ -18351,6 +18366,7 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int NT_RADIO_TECHNOLOGY_UNKNOWN = 0; // 0x0 field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT = 2; // 0x2 field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; // 0x1 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER = 0; // 0x0 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE = 0; // 0x0 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED = 7; // 0x7 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_NONE = 6; // 0x6 @@ -18364,6 +18380,8 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_CONNECTED = 7; // 0x7 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_DATAGRAM_RETRYING = 3; // 0x3 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_MODEM_STATE_DISABLING_SATELLITE = 9; // 0x9 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8; // 0x8 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_IDLE = 0; // 0x0 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_LISTENING = 1; // 0x1 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_NOT_CONNECTED = 6; // 0x6 @@ -18371,11 +18389,16 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_UNAVAILABLE = 5; // 0x5 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1; // 0xffffffff field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ACCESS_BARRED = 16; // 0x10 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_DISABLE_IN_PROGRESS = 28; // 0x1c + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS = 27; // 0x1b + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_ENABLE_IN_PROGRESS = 29; // 0x1d field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ERROR = 1; // 0x1 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; // 0x17 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_ARGUMENTS = 8; // 0x8 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_MODEM_STATE = 7; // 0x7 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; // 0x6 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_LOCATION_DISABLED = 25; // 0x19 + field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_LOCATION_NOT_AVAILABLE = 26; // 0x1a field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_BUSY = 22; // 0x16 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_ERROR = 4; // 0x4 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24; // 0x18 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 117351943587..c8ecfa94ec87 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -535,9 +535,13 @@ package android.app { public class WallpaperManager { method @Nullable public android.graphics.Bitmap getBitmap(); method @Nullable public android.graphics.Bitmap getBitmapAsUser(int, boolean, int); + method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull java.util.List<android.graphics.Point>, int, boolean); + method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull android.graphics.Point, @NonNull java.util.List<android.graphics.Point>, @Nullable java.util.Map<android.graphics.Point,android.graphics.Rect>); method public boolean isLockscreenLiveWallpaperEnabled(); method @Nullable public android.graphics.Rect peekBitmapDimensions(); method @Nullable public android.graphics.Rect peekBitmapDimensions(int); + method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setBitmapWithCrops(@Nullable android.graphics.Bitmap, @NonNull java.util.Map<android.graphics.Point,android.graphics.Rect>, boolean, int) throws java.io.IOException; + method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setStreamWithCrops(@NonNull java.io.InputStream, @NonNull java.util.Map<android.graphics.Point,android.graphics.Rect>, boolean, int) throws java.io.IOException; method public void setWallpaperZoomOut(@NonNull android.os.IBinder, float); method public boolean shouldEnableWideColorGamut(); method public boolean wallpaperSupportsWcg(int); @@ -3247,6 +3251,14 @@ package android.service.quicksettings { } +package android.service.settings.preferences { + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable { + ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String, boolean, @Nullable android.content.ServiceConnection); + } + +} + package android.service.voice { public class AlwaysOnHotwordDetector implements android.service.voice.HotwordDetector { @@ -3729,7 +3741,7 @@ package android.view { method public final int getDisplayId(); method public final void setDisplayId(int); field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800 - field public static final int LAST_KEYCODE = 318; // 0x13e + field public static final int LAST_KEYCODE = 337; // 0x151 } public final class KeyboardShortcutGroup implements android.os.Parcelable { diff --git a/core/java/Android.bp b/core/java/Android.bp index 71623c566501..cf5ebbaa37b4 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -28,6 +28,7 @@ filegroup { exclude_srcs: [ "android/os/*MessageQueue/**/*.java", "android/ranging/**/*.java", + ":dynamic_instrumentation_manager_aidl_sources", ], visibility: ["//frameworks/base"], } @@ -120,6 +121,17 @@ filegroup { } filegroup { + name: "dynamic_instrumentation_manager_aidl_sources", + srcs: ["android/os/instrumentation/*.aidl"], +} + +aidl_interface { + name: "dynamic_instrumentation_manager_aidl", + srcs: [":dynamic_instrumentation_manager_aidl_sources"], + unstable: true, +} + +filegroup { name: "framework-internal-display-sources", srcs: ["com/android/internal/display/BrightnessSynchronizer.java"], visibility: ["//frameworks/base/services/tests/mockingservicestests"], @@ -685,16 +697,31 @@ gen_readonly_feature_apis = select(release_flag("RELEASE_USE_SYSTEM_FEATURE_BUIL // Generates com.android.internal.pm.RoSystemFeatures, optionally compiling in // details about fixed system features defined by build flags. When disabled, // the APIs are simply passthrough stubs with no meaningful side effects. +// TODO(b/203143243): Implement the `--feature=` aggregation directly with a native soong module. genrule { name: "systemfeatures-gen-srcs", cmd: "$(location systemfeatures-gen-tool) com.android.internal.pm.RoSystemFeatures " + // --readonly=false (default) makes the codegen an effective no-op passthrough API. " --readonly=" + gen_readonly_feature_apis + - // For now, only export "android.hardware.type.*" system features APIs. - // TODO(b/203143243): Use an intermediate soong var that aggregates all declared - // RELEASE_SYSTEM_FEATURE_* declarations into a single arg. - " --feature-apis=AUTOMOTIVE,WATCH,TELEVISION,EMBEDDED,PC" + - " > $(out)", + " --feature=AUTOMOTIVE:" + select(release_flag("RELEASE_SYSTEM_FEATURE_AUTOMOTIVE"), { + any @ value: value, + default: "", + }) + " --feature=EMBEDDED:" + select(release_flag("RELEASE_SYSTEM_FEATURE_EMBEDDED"), { + any @ value: value, + default: "", + }) + " --feature=LEANBACK:" + select(release_flag("RELEASE_SYSTEM_FEATURE_LEANBACK"), { + any @ value: value, + default: "", + }) + " --feature=PC:" + select(release_flag("RELEASE_SYSTEM_FEATURE_PC"), { + any @ value: value, + default: "", + }) + " --feature=TELEVISION:" + select(release_flag("RELEASE_SYSTEM_FEATURE_TELEVISION"), { + any @ value: value, + default: "", + }) + " --feature=WATCH:" + select(release_flag("RELEASE_SYSTEM_FEATURE_WATCH"), { + any @ value: value, + default: "", + }) + " > $(out)", out: [ "RoSystemFeatures.java", ], diff --git a/core/java/android/adaptiveauth/OWNERS b/core/java/android/adaptiveauth/OWNERS index 0218a7835586..bc8efa92c16f 100644 --- a/core/java/android/adaptiveauth/OWNERS +++ b/core/java/android/adaptiveauth/OWNERS @@ -1 +1 @@ -include /services/core/java/com/android/server/adaptiveauth/OWNERS
\ No newline at end of file +include /services/core/java/com/android/server/security/adaptiveauthentication/OWNERS
\ No newline at end of file diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 7a1c759a3ec4..3fccc17e1bf1 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -30,6 +30,7 @@ import static com.android.sdksandbox.flags.Flags.sandboxActivitySdkBasedContext; import static java.lang.Character.MIN_VALUE; +import android.Manifest; import android.annotation.AnimRes; import android.annotation.CallSuper; import android.annotation.CallbackExecutor; @@ -3193,6 +3194,16 @@ public class Activity extends ContextThemeWrapper return ActivityTaskManager.getMaxNumPictureInPictureActions(this); } + private boolean isImplicitEnterPipProhibited() { + PackageManager pm = getPackageManager(); + if (android.app.Flags.enableTvImplicitEnterPipRestriction()) { + return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + && pm.checkPermission(Manifest.permission.TV_IMPLICIT_ENTER_PIP, + getPackageName()) == PackageManager.PERMISSION_DENIED; + } + return false; + } + /** * @return Whether this device supports picture-in-picture. */ @@ -9192,6 +9203,8 @@ public class Activity extends ContextThemeWrapper } dispatchActivityPreResumed(); + mCanEnterPictureInPicture = true; + mFragments.execPendingActions(); mLastNonConfigurationInstances = null; @@ -9243,6 +9256,11 @@ public class Activity extends ContextThemeWrapper Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "performPause:" + mComponent.getClassName()); } + + if (isImplicitEnterPipProhibited()) { + mCanEnterPictureInPicture = false; + } + dispatchActivityPrePaused(); mDoReportFullyDrawn = false; mFragments.dispatchPause(); @@ -9265,6 +9283,10 @@ public class Activity extends ContextThemeWrapper final void performUserLeaving() { onUserInteraction(); + + if (isImplicitEnterPipProhibited()) { + mCanEnterPictureInPicture = false; + } onUserLeaveHint(); } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 38c8583dd024..fd70f4fc3f25 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1621,9 +1621,12 @@ public class AppOpsManager { */ public static final int OP_RANGING = AppOpEnums.APP_OP_RANGING; + /** @hide Access to read oxygen saturation. */ + public static final int OP_READ_OXYGEN_SATURATION = AppOpEnums.APP_OP_READ_OXYGEN_SATURATION; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 152; + public static final int _NUM_OP = 153; /** * All app ops represented as strings. @@ -1779,6 +1782,7 @@ public class AppOpsManager { OPSTR_READ_HEART_RATE, OPSTR_READ_SKIN_TEMPERATURE, OPSTR_RANGING, + OPSTR_READ_OXYGEN_SATURATION, }) public @interface AppOpString {} @@ -2521,6 +2525,11 @@ public class AppOpsManager { @FlaggedApi(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED) public static final String OPSTR_READ_HEART_RATE = "android:read_heart_rate"; + /** @hide Access to read oxygen saturation. */ + @SystemApi + @FlaggedApi(Flags.FLAG_PLATFORM_OXYGEN_SATURATION_ENABLED) + public static final String OPSTR_READ_OXYGEN_SATURATION = "android:read_oxygen_saturation"; + /** @hide Access to read skin temperature. */ @SystemApi @FlaggedApi(Flags.FLAG_PLATFORM_SKIN_TEMPERATURE_ENABLED) @@ -2608,6 +2617,7 @@ public class AppOpsManager { // Health Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE, Flags.platformSkinTemperatureEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE, + Flags.platformOxygenSaturationEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE, }; /** @@ -3129,6 +3139,11 @@ public class AppOpsManager { .setPermission(Flags.rangingPermissionEnabled()? Manifest.permission.RANGING : null) .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), + new AppOpInfo.Builder(OP_READ_OXYGEN_SATURATION, OPSTR_READ_OXYGEN_SATURATION, + "READ_OXYGEN_SATURATION").setPermission( + Flags.platformOxygenSaturationEnabled() + ? HealthPermissions.READ_OXYGEN_SATURATION : null) + .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), }; // The number of longs needed to form a full bitmask of app ops diff --git a/core/java/android/app/DisabledWallpaperManager.java b/core/java/android/app/DisabledWallpaperManager.java index b06fb9e2f284..233dc75b810f 100644 --- a/core/java/android/app/DisabledWallpaperManager.java +++ b/core/java/android/app/DisabledWallpaperManager.java @@ -177,6 +177,13 @@ final class DisabledWallpaperManager extends WallpaperManager { } @Override + @NonNull + public SparseArray<Rect> getBitmapCrops(int which) { + unsupported(); + return new SparseArray<>(); + } + + @Override public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes, @Nullable Map<Point, Rect> cropHints) { return unsupported(); @@ -358,8 +365,9 @@ final class DisabledWallpaperManager extends WallpaperManager { @Override - public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints, - boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + public int setStreamWithCrops(@NonNull InputStream bitmapData, + @NonNull SparseArray<Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which + ) throws IOException { return unsupportedInt(); } diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index f693e9ba11ec..6449ea1742a1 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -97,6 +97,16 @@ interface IWallpaperManager { List getBitmapCrops(in List<Point> displaySizes, int which, boolean originalBitmap, int userId); /** + * For a given user, if the wallpaper of the specified which is an ImageWallpaper, return + * a bundle which is a Map<Integer, Rect> containing the custom cropHints that were sent to + * setBitmapWithCrops or setStreamWithCrops. These crops are relative to the original bitmap. + * If the wallpaper isn't an ImageWallpaper, return null. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL)") + @SuppressWarnings(value={"untyped-collection"}) + Bundle getCurrentBitmapCrops(int which, int userId); + + /** * Return how a bitmap of a given size would be cropped for a given list of display sizes when * set with the given suggested crops. * @hide diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index f432a22b14ac..1dc774285a32 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -32,7 +32,10 @@ import android.os.Process; import android.os.SystemClock; import android.os.SystemProperties; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Log; +import android.util.SparseArray; +import android.util.SparseBooleanArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -92,9 +95,6 @@ public class PropertyInvalidatedCache<Query, Result> { * caching on behalf of other processes. */ public boolean shouldBypassCache(@NonNull Q query) { - if(android.multiuser.Flags.propertyInvalidatedCacheBypassMismatchedUids()) { - return Binder.getCallingUid() != Process.myUid(); - } return false; } }; @@ -392,8 +392,213 @@ public class PropertyInvalidatedCache<Query, Result> { } } + /** + * An array of hash maps, indexed by calling UID. The class behaves a bit like a hash map + * except that it uses the calling UID internally. + */ + private class CacheMap<Query, Result> { + + // Create a new map for a UID, using the parent's configuration for max size. + private LinkedHashMap<Query, Result> createMap() { + return new LinkedHashMap<Query, Result>( + 2 /* start small */, + 0.75f /* default load factor */, + true /* LRU access order */) { + @GuardedBy("mLock") + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + final int size = size(); + if (size > mHighWaterMark) { + mHighWaterMark = size; + } + if (size > mMaxEntries) { + mMissOverflow++; + return true; + } + return false; + } + }; + } + + // An array of maps, indexed by UID. + private final SparseArray<LinkedHashMap<Query, Result>> mCache = new SparseArray<>(); + + // If true, isolate the hash entries by calling UID. If this is false, allow the cache + // entries to be combined in a single hash map. + private final boolean mIsolated; + + // Collect statistics. + private final boolean mStatistics; + + // An array of booleans to indicate if a UID has been involved in a map access. A value + // exists for every UID that was ever involved during cache access. This is updated only + // if statistics are being collected. + private final SparseBooleanArray mUidSeen; + + // A hash map that ignores the UID. This is used in look-aside fashion just for hit/miss + // statistics. This is updated only if statistics are being collected. + private final ArraySet<Query> mShadowCache; + + // Shadow statistics. Only hits and misses need to be recorded. These are updated only + // if statistics are being collected. The "SelfHits" records hits when the UID is the + // process uid. + private int mShadowHits; + private int mShadowMisses; + private int mShadowSelfHits; + + // The process UID. + private final int mSelfUid; + + // True in test mode. In test mode, the cache uses Binder.getWorkSource() as the UID. + private final boolean mTestMode; + + /** + * Create a CacheMap. UID isolation is enabled if the input parameter is true and if the + * isolation feature is enabled. + */ + CacheMap(boolean isolate, boolean testMode) { + mIsolated = Flags.picIsolateCacheByUid() && isolate; + mStatistics = Flags.picIsolatedCacheStatistics() && mIsolated; + if (mStatistics) { + mUidSeen = new SparseBooleanArray(); + mShadowCache = new ArraySet<>(); + } else { + mUidSeen = null; + mShadowCache = null; + } + mSelfUid = Process.myUid(); + mTestMode = testMode; + } + + // Return the UID for this cache invocation. If uid isolation is disabled, the value of 0 + // is returned, which effectively places all entries in a single hash map. + private int callerUid() { + if (!mIsolated) { + return 0; + } else if (mTestMode) { + return Binder.getCallingWorkSourceUid(); + } else { + return Binder.getCallingUid(); + } + } + + /** + * Lookup an entry in the cache. + */ + Result get(Query query) { + final int uid = callerUid(); + + // Shadow statistics + if (mStatistics) { + if (mShadowCache.contains(query)) { + mShadowHits++; + if (uid == mSelfUid) { + mShadowSelfHits++; + } + } else { + mShadowMisses++; + } + } + + var map = mCache.get(uid); + if (map != null) { + return map.get(query); + } else { + return null; + } + } + + /** + * Remove an entry from the cache. + */ + void remove(Query query) { + final int uid = callerUid(); + if (mStatistics) { + mShadowCache.remove(query); + } + + var map = mCache.get(uid); + if (map != null) { + map.remove(query); + } + } + + /** + * Record an entry in the cache. + */ + void put(Query query, Result result) { + final int uid = callerUid(); + if (mStatistics) { + mShadowCache.add(query); + mUidSeen.put(uid, true); + } + + var map = mCache.get(uid); + if (map == null) { + map = createMap(); + mCache.put(uid, map); + } + map.put(query, result); + } + + /** + * Return the number of entries in the cache. + */ + int size() { + int total = 0; + for (int i = 0; i < mCache.size(); i++) { + var map = mCache.valueAt(i); + total += map.size(); + } + return total; + } + + /** + * Clear the entries in the cache. Update the shadow statistics. + */ + void clear() { + if (mStatistics) { + mShadowCache.clear(); + } + + mCache.clear(); + } + + // Dump basic statistics, if any are collected. Do nothing if statistics are not enabled. + void dump(PrintWriter pw) { + if (mStatistics) { + pw.println(formatSimple(" ShadowHits: %d, ShadowMisses: %d, ShadowSize: %d", + mShadowHits, mShadowMisses, mShadowCache.size())); + pw.println(formatSimple(" ShadowUids: %d, SelfUid: %d", + mUidSeen.size(), mShadowSelfHits)); + } + } + + // Dump detailed statistics + void dumpDetailed(PrintWriter pw) { + for (int i = 0; i < mCache.size(); i++) { + int uid = mCache.keyAt(i); + var map = mCache.valueAt(i); + + Set<Map.Entry<Query, Result>> cacheEntries = map.entrySet(); + if (cacheEntries.size() == 0) { + break; + } + + pw.println(" Contents:"); + pw.println(formatSimple(" Uid: %d\n", uid)); + for (Map.Entry<Query, Result> entry : cacheEntries) { + String key = Objects.toString(entry.getKey()); + String value = Objects.toString(entry.getValue()); + + pw.println(formatSimple(" Key: %s\n Value: %s\n", key, value)); + } + } + } + } + @GuardedBy("mLock") - private final LinkedHashMap<Query, Result> mCache; + private final CacheMap<Query, Result> mCache; /** * The nonce handler for this cache. @@ -895,7 +1100,8 @@ public class PropertyInvalidatedCache<Query, Result> { * is allowed to be null in the record constructor to facility reuse of Args instances. * @hide */ - public static record Args(@NonNull String mModule, @Nullable String mApi, int mMaxEntries) { + public static record Args(@NonNull String mModule, @Nullable String mApi, + int mMaxEntries, boolean mIsolateUids, boolean mTestMode) { // Validation: the module must be one of the known module strings and the maxEntries must // be positive. @@ -909,15 +1115,28 @@ public class PropertyInvalidatedCache<Query, Result> { // which is not legal, but there is no reasonable default. Clients must call the api // method to set the field properly. public Args(@NonNull String module) { - this(module, /* api */ null, /* maxEntries */ 32); + this(module, + null, // api + 32, // maxEntries + true, // isolateUids + false // testMode + ); } public Args api(@NonNull String api) { - return new Args(mModule, api, mMaxEntries); + return new Args(mModule, api, mMaxEntries, mIsolateUids, mTestMode); } public Args maxEntries(int val) { - return new Args(mModule, mApi, val); + return new Args(mModule, mApi, val, mIsolateUids, mTestMode); + } + + public Args isolateUids(boolean val) { + return new Args(mModule, mApi, mMaxEntries, val, mTestMode); + } + + public Args testMode(boolean val) { + return new Args(mModule, mApi, mMaxEntries, mIsolateUids, val); } } @@ -936,7 +1155,7 @@ public class PropertyInvalidatedCache<Query, Result> { mCacheName = cacheName; mNonce = getNonceHandler(mPropertyName); mMaxEntries = args.mMaxEntries; - mCache = createMap(); + mCache = new CacheMap<>(args.mIsolateUids, args.mTestMode); mComputer = (computer != null) ? computer : new DefaultComputer<>(this); registerCache(); } @@ -1006,28 +1225,6 @@ public class PropertyInvalidatedCache<Query, Result> { this(new Args(module).maxEntries(maxEntries).api(api), cacheName, computer); } - // Create a map. This should be called only from the constructor. - private LinkedHashMap<Query, Result> createMap() { - return new LinkedHashMap<Query, Result>( - 2 /* start small */, - 0.75f /* default load factor */, - true /* LRU access order */) { - @GuardedBy("mLock") - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - final int size = size(); - if (size > mHighWaterMark) { - mHighWaterMark = size; - } - if (size > mMaxEntries) { - mMissOverflow++; - return true; - } - return false; - } - }; - } - /** * Register the map in the global list. If the cache is disabled globally, disable it * now. This method is only ever called from the constructor, which means no other thread has @@ -1778,8 +1975,8 @@ public class PropertyInvalidatedCache<Query, Result> { pw.println(formatSimple(" Cache Name: %s", cacheName())); pw.println(formatSimple(" Property: %s", mPropertyName)); pw.println(formatSimple( - " Hits: %d, Misses: %d, Skips: %d, Clears: %d", - mHits, mMisses, getSkipsLocked(), mClears)); + " Hits: %d, Misses: %d, Skips: %d, Clears: %d, Uids: %d", + mHits, mMisses, getSkipsLocked(), mClears, mCache.size())); // Print all the skip reasons. pw.format(" Skip-%s: %d", sNonceName[0], mSkips[0]); @@ -1794,25 +1991,16 @@ public class PropertyInvalidatedCache<Query, Result> { pw.println(formatSimple( " Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d", mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow)); + mCache.dump(pw); pw.println(formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); - // No specific cache was requested. This is the default, and no details - // should be dumped. - if (!detailed) { - return; - } - Set<Map.Entry<Query, Result>> cacheEntries = mCache.entrySet(); - if (cacheEntries.size() == 0) { - return; + // Dump the contents of the cache. + if (detailed) { + mCache.dumpDetailed(pw); } - pw.println(" Contents:"); - for (Map.Entry<Query, Result> entry : cacheEntries) { - String key = Objects.toString(entry.getKey()); - String value = Objects.toString(entry.getValue()); - - pw.println(formatSimple(" Key: %s\n Value: %s\n", key, value)); - } + // Separator between caches. + pw.println(""); } } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 479f3df9affb..abb2dd465576 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -19,9 +19,10 @@ package android.app; import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_WALLPAPER_INTERNAL; import static android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT; +import static android.app.Flags.FLAG_CUSTOMIZATION_PACKS_APIS; +import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; -import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; import static com.android.window.flags.Flags.FLAG_MULTI_CROP; import static com.android.window.flags.Flags.multiCrop; @@ -342,24 +343,32 @@ public class WallpaperManager { * Portrait orientation of most screens * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi public static final int ORIENTATION_PORTRAIT = 0; /** * Landscape orientation of most screens * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi public static final int ORIENTATION_LANDSCAPE = 1; /** * Portrait orientation with similar width and height (e.g. the inner screen of a foldable) * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi public static final int ORIENTATION_SQUARE_PORTRAIT = 2; /** * Landscape orientation with similar width and height (e.g. the inner screen of a foldable) * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi public static final int ORIENTATION_SQUARE_LANDSCAPE = 3; /** @@ -368,7 +377,9 @@ public class WallpaperManager { * @return the corresponding {@link ScreenOrientation}. * @hide */ - public static @ScreenOrientation int getOrientation(Point screenSize) { + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi + public static @ScreenOrientation int getOrientation(@NonNull Point screenSize) { float ratio = ((float) screenSize.x) / screenSize.y; // ratios between 3/4 and 4/3 are considered square return ratio >= 4 / 3f ? ORIENTATION_LANDSCAPE @@ -1623,14 +1634,15 @@ public class WallpaperManager { * If false, return areas relative to the cropped bitmap. * @return A List of Rect where the Rect is within the cropped/original bitmap, and corresponds * to what is displayed. The Rect may have a larger width/height ratio than the screen - * due to parallax. Return {@code null} if the wallpaper is not an ImageWallpaper. - * Also return {@code null} when called with which={@link #FLAG_LOCK} if there is a + * due to parallax. Return an empty list if the wallpaper is not an ImageWallpaper. + * Also return an empty list when called with which={@link #FLAG_LOCK} if there is a * shared home + lock wallpaper. * @hide */ @FlaggedApi(FLAG_MULTI_CROP) + @TestApi @RequiresPermission(READ_WALLPAPER_INTERNAL) - @Nullable + @NonNull public List<Rect> getBitmapCrops(@NonNull List<Point> displaySizes, @SetWallpaperFlags int which, boolean originalBitmap) { checkExactlyOneWallpaperFlagSet(which); @@ -1653,6 +1665,52 @@ public class WallpaperManager { } /** + * For the current user, if the wallpaper of the specified destination is an ImageWallpaper, + * return the custom crops of the wallpaper, that have been provided for example via + * {@link #setStreamWithCrops}. These crops are relative to the original bitmap. + * <p> + * This method helps apps that change wallpapers provide an undo option. Calling + * {@link #setStreamWithCrops(InputStream, SparseArray, boolean, int)} with this SparseArray and + * the current original bitmap file, that can be obtained with {@link #getWallpaperFile(int, + * boolean)} with {@code getCropped=false}, will exactly lead to the current wallpaper state. + * + * @param which wallpaper type. Must be either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. + * @return A map from {{@link #ORIENTATION_PORTRAIT}, {@link #ORIENTATION_LANDSCAPE}, + * {@link #ORIENTATION_SQUARE_PORTRAIT}, {{@link #ORIENTATION_SQUARE_LANDSCAPE}}} to + * Rect, representing the custom cropHints. The map can be empty and will only contains + * entries for screen orientations for which a custom crop was provided. If no custom + * crop is provided for an orientation, the system will infer the crop based on the + * custom crops of the other orientations; or center-align the full image if no custom + * crops are provided at all. + * <p> + * Return an empty map if the wallpaper is not an ImageWallpaper. Also return + * an empty map when called with which={@link #FLAG_LOCK} if there is a shared + * home + lock wallpaper. + * + * @hide + */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi + @RequiresPermission(READ_WALLPAPER_INTERNAL) + @NonNull + public SparseArray<Rect> getBitmapCrops(@SetWallpaperFlags int which) { + checkExactlyOneWallpaperFlagSet(which); + try { + Bundle bundle = sGlobals.mService.getCurrentBitmapCrops(which, mContext.getUserId()); + SparseArray<Rect> result = new SparseArray<>(); + if (bundle == null) return result; + for (String key : bundle.keySet()) { + int intKey = Integer.parseInt(key); + Rect rect = bundle.getParcelable(key, Rect.class); + result.put(intKey, rect); + } + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * For preview purposes. * Return how a bitmap of a given size would be cropped for a given list of display sizes, if * it was set as wallpaper via {@link #setBitmapWithCrops(Bitmap, Map, boolean, int)} or @@ -1664,7 +1722,8 @@ public class WallpaperManager { * @hide */ @FlaggedApi(FLAG_MULTI_CROP) - @Nullable + @TestApi + @NonNull public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes, @Nullable Map<Point, Rect> cropHints) { try { @@ -1890,9 +1949,14 @@ public class WallpaperManager { * defined kind of wallpaper, either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. * @param getCropped If true the cropped file will be retrieved, if false the original will * be retrieved. - * + * @return A ParcelFileDescriptor for the wallpaper bitmap of the given destination, if it's an + * ImageWallpaper wallpaper. Return {@code null} if the wallpaper is not an + * ImageWallpaper. Also return {@code null} when called with + * which={@link #FLAG_LOCK} if there is a shared home + lock wallpaper. * @hide */ + @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) + @SystemApi @Nullable public ParcelFileDescriptor getWallpaperFile(@SetWallpaperFlags int which, boolean getCropped) { return getWallpaperFile(which, mContext.getUserId(), getCropped); @@ -2371,7 +2435,6 @@ public class WallpaperManager { /** * Version of setBitmap that defines how the wallpaper will be positioned for different * display sizes. - * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. * @param cropHints map from screen dimensions to a sub-region of the image to display for those * dimensions. The {@code Rect} sub-region may have a larger width/height ratio * than the screen dimensions to apply a horizontal parallax effect. If the @@ -2380,6 +2443,7 @@ public class WallpaperManager { * @hide */ @FlaggedApi(FLAG_MULTI_CROP) + @TestApi @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull Map<Point, Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which) throws IOException { @@ -2562,7 +2626,6 @@ public class WallpaperManager { /** * Version of setStream that defines how the wallpaper will be positioned for different * display sizes. - * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. * @param cropHints map from screen dimensions to a sub-region of the image to display for those * dimensions. The {@code Rect} sub-region may have a larger width/height ratio * than the screen dimensions to apply a horizontal parallax effect. If the @@ -2571,9 +2634,11 @@ public class WallpaperManager { * @hide */ @FlaggedApi(FLAG_MULTI_CROP) + @TestApi @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) - public int setStreamWithCrops(InputStream bitmapData, @NonNull Map<Point, Rect> cropHints, - boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + public int setStreamWithCrops(@NonNull InputStream bitmapData, + @NonNull Map<Point, Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which) + throws IOException { SparseArray<Rect> crops = new SparseArray<>(); cropHints.forEach((k, v) -> crops.put(getOrientation(k), v)); return setStreamWithCrops(bitmapData, crops, allowBackup, which); @@ -2583,15 +2648,21 @@ public class WallpaperManager { * Similar to {@link #setStreamWithCrops(InputStream, Map, boolean, int)}, but using * {@link ScreenOrientation} as keys of the cropHints map. Used for backup & restore, since * WallpaperBackupAgent stores orientations rather than the exact display size. - * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}. + * @param bitmapData A stream containing the raw data to install as a wallpaper. This + * data can be in any format handled by {@link BitmapRegionDecoder}. * @param cropHints map from {@link ScreenOrientation} to a sub-region of the image to display * for that screen orientation. + * @param allowBackup {@code true} if the OS is permitted to back up this wallpaper + * image for restore to a future device; {@code false} otherwise. + * @param which Flags indicating which wallpaper(s) to configure with the new imagery. * @hide */ @FlaggedApi(FLAG_MULTI_CROP) + @SystemApi @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) - public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints, - boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + public int setStreamWithCrops(@NonNull InputStream bitmapData, + @NonNull SparseArray<Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which) + throws IOException { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); throw new RuntimeException(new DeadSystemException()); diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index fee071b14016..be24bfa41e10 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -367,3 +367,10 @@ flag { description: "Allows DPMS to enable or disable SupervisionService based on whether the device is being managed by the supervision role holder." bug: "376213673" } + +flag { + name: "split_create_managed_profile_enabled" + namespace: "enterprise" + description: "Split up existing create and provision managed profile API." + bug: "375382324" +} diff --git a/core/java/android/app/appfunctions/AppFunctionException.aidl b/core/java/android/app/appfunctions/AppFunctionException.aidl new file mode 100644 index 000000000000..7d432243b1b2 --- /dev/null +++ b/core/java/android/app/appfunctions/AppFunctionException.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +package android.app.appfunctions; + +import android.app.appfunctions.AppFunctionException; + +parcelable AppFunctionException;
\ No newline at end of file diff --git a/core/java/android/app/appfunctions/AppFunctionException.java b/core/java/android/app/appfunctions/AppFunctionException.java new file mode 100644 index 000000000000..cbd1d932ab00 --- /dev/null +++ b/core/java/android/app/appfunctions/AppFunctionException.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.appfunctions; + +import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** Represents an app function related errors. */ +@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) +public final class AppFunctionException extends Exception implements Parcelable { + /** + * The caller does not have the permission to execute an app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_DENIED = 1000; + + /** + * The caller supplied invalid arguments to the execution request. + * + * <p>This error may be considered similar to {@link IllegalArgumentException}. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_INVALID_ARGUMENT = 1001; + + /** + * The caller tried to execute a disabled app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_DISABLED = 1002; + + /** + * The caller tried to execute a function that does not exist. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_FUNCTION_NOT_FOUND = 1003; + + /** + * An internal unexpected error coming from the system. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int ERROR_SYSTEM_ERROR = 2000; + + /** + * The operation was cancelled. Use this error code to report that a cancellation is done after + * receiving a cancellation signal. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int ERROR_CANCELLED = 2001; + + /** + * An unknown error occurred while processing the call in the AppFunctionService. + * + * <p>This error is thrown when the service is connected in the remote application but an + * unexpected error is thrown from the bound application. + * + * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. + */ + public static final int ERROR_APP_UNKNOWN_ERROR = 3000; + + /** + * The error category is unknown. + * + * <p>This is the default value for {@link #getErrorCategory}. + */ + public static final int ERROR_CATEGORY_UNKNOWN = 0; + + /** + * The error is caused by the app requesting a function execution. + * + * <p>For example, the caller provided invalid parameters in the execution request e.g. an + * invalid function ID. + * + * <p>Errors in the category fall in the range 1000-1999 inclusive. + */ + public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; + + /** + * The error is caused by an issue in the system. + * + * <p>For example, the AppFunctionService implementation is not found by the system. + * + * <p>Errors in the category fall in the range 2000-2999 inclusive. + */ + public static final int ERROR_CATEGORY_SYSTEM = 2; + + /** + * The error is caused by the app providing the function. + * + * <p>For example, the app crashed when the system is executing the request. + * + * <p>Errors in the category fall in the range 3000-3999 inclusive. + */ + public static final int ERROR_CATEGORY_APP = 3; + + private final int mErrorCode; + @Nullable private final String mErrorMessage; + @NonNull private final Bundle mExtras; + + /** + * @param errorCode The error code. + * @param errorMessage The error message. + */ + public AppFunctionException(@ErrorCode int errorCode, @Nullable String errorMessage) { + this(errorCode, errorMessage, Bundle.EMPTY); + } + + /** + * @param errorCode The error code. + * @param errorMessage The error message. + * @param extras The extras associated with this error. + */ + public AppFunctionException( + @ErrorCode int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) { + super(errorMessage); + mErrorCode = errorCode; + mErrorMessage = errorMessage; + mExtras = Objects.requireNonNull(extras); + } + + private AppFunctionException(@NonNull Parcel in) { + mErrorCode = in.readInt(); + mErrorMessage = in.readString8(); + mExtras = Objects.requireNonNull(in.readBundle(getClass().getClassLoader())); + } + + /** Returns one of the {@code ERROR} constants. */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** Returns the error message. */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + /** + * Returns the error category. + * + * <p>This method categorizes errors based on their underlying cause, allowing developers to + * implement targeted error handling and provide more informative error messages to users. It + * maps ranges of error codes to specific error categories. + * + * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the error code does not belong to + * any error category. + * + * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding + * error code ranges. + */ + @ErrorCategory + public int getErrorCategory() { + if (mErrorCode >= 1000 && mErrorCode < 2000) { + return ERROR_CATEGORY_REQUEST_ERROR; + } + if (mErrorCode >= 2000 && mErrorCode < 3000) { + return ERROR_CATEGORY_SYSTEM; + } + if (mErrorCode >= 3000 && mErrorCode < 4000) { + return ERROR_CATEGORY_APP; + } + return ERROR_CATEGORY_UNKNOWN; + } + + /** Returns any extras associated with this error. */ + @NonNull + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mErrorCode); + dest.writeString8(mErrorMessage); + dest.writeBundle(mExtras); + } + + /** + * Error codes. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_"}, + value = { + ERROR_DENIED, + ERROR_APP_UNKNOWN_ERROR, + ERROR_FUNCTION_NOT_FOUND, + ERROR_SYSTEM_ERROR, + ERROR_INVALID_ARGUMENT, + ERROR_DISABLED, + ERROR_CANCELLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCode {} + + /** + * Error categories. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_CATEGORY_"}, + value = { + ERROR_CATEGORY_UNKNOWN, + ERROR_CATEGORY_REQUEST_ERROR, + ERROR_CATEGORY_APP, + ERROR_CATEGORY_SYSTEM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCategory {} + + @NonNull + public static final Creator<AppFunctionException> CREATOR = + new Creator<>() { + @Override + public AppFunctionException createFromParcel(Parcel in) { + return new AppFunctionException(in); + } + + @Override + public AppFunctionException[] newArray(int size) { + return new AppFunctionException[size]; + } + }; +} diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 5ddb590add4c..ed088fed41c2 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -16,7 +16,7 @@ package android.app.appfunctions; -import static android.app.appfunctions.ExecuteAppFunctionResponse.getResultCode; +import static android.app.appfunctions.AppFunctionException.ERROR_SYSTEM_ERROR; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; import android.Manifest; @@ -39,7 +39,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; -import java.util.function.Consumer; /** * Provides access to app functions. @@ -147,16 +146,16 @@ public final class AppFunctionManager { * @param request the request to execute the app function * @param executor the executor to run the callback * @param cancellationSignal the cancellation signal to cancel the execution. - * @param callback the callback to receive the function execution result. + * @param callback the callback to receive the function execution result or error. * <p>If the calling app does not own the app function or does not have {@code * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code - * ExecuteAppFunctionResponse.RESULT_DENIED}. + * AppFunctionException.ERROR_DENIED}. * <p>If the caller only has {@code android.permission.EXECUTE_APP_FUNCTIONS} but the * function requires {@code android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED}, the execution - * result will contain {@code ExecuteAppFunctionResponse.RESULT_DENIED} + * result will contain {@code AppFunctionException.ERROR_DENIED} * <p>If the function requested for execution is disabled, then the execution result will - * contain {@code ExecuteAppFunctionResponse.RESULT_DISABLED} + * contain {@code AppFunctionException.ERROR_DISABLED} * <p>If the cancellation signal is issued, the operation is cancelled and no response is * returned to the caller. */ @@ -171,7 +170,9 @@ public final class AppFunctionManager { @NonNull ExecuteAppFunctionRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + @NonNull + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> + callback) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -186,20 +187,25 @@ public final class AppFunctionManager { aidlRequest, new IExecuteAppFunctionCallback.Stub() { @Override - public void onResult(ExecuteAppFunctionResponse result) { + public void onSuccess(ExecuteAppFunctionResponse result) { try { - executor.execute(() -> callback.accept(result)); + executor.execute(() -> callback.onResult(result)); } catch (RuntimeException e) { // Ideally shouldn't happen since errors are wrapped into - // the - // response, but we catch it here for additional safety. - callback.accept( - ExecuteAppFunctionResponse.newFailure( - getResultCode(e), - e.getMessage(), - /* extras= */ null)); + // the response, but we catch it here for additional safety. + executor.execute( + () -> + callback.onError( + new AppFunctionException( + ERROR_SYSTEM_ERROR, + e.getMessage()))); } } + + @Override + public void onError(AppFunctionException exception) { + executor.execute(() -> callback.onError(exception)); + } }); if (cancellationTransport != null) { cancellationSignal.setRemote(cancellationTransport); diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java index 06d95f5270c3..3ddda228d145 100644 --- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java +++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java @@ -213,9 +213,7 @@ public class AppFunctionRuntimeMetadata extends GenericDocument { setEnabled(original.getEnabled()); } - /** - * Sets an indicator specifying the function enabled state. - */ + /** Sets an indicator specifying the function enabled state. */ @NonNull public Builder setEnabled(@EnabledState int enabledState) { if (enabledState != APP_FUNCTION_STATE_DEFAULT diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java index 63d187aa11ef..85b6ab2b4e61 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -17,7 +17,6 @@ package android.app.appfunctions; import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE; -import static android.app.appfunctions.ExecuteAppFunctionResponse.getResultCode; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; import static android.content.pm.PackageManager.PERMISSION_DENIED; @@ -32,10 +31,8 @@ import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; import android.os.ICancellationSignal; +import android.os.OutcomeReceiver; import android.os.RemoteException; -import android.util.Log; - -import java.util.function.Consumer; /** * Abstract base class to provide app functions to the system. @@ -80,7 +77,9 @@ public abstract class AppFunctionService extends Service { @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback); + @NonNull + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> + callback); } /** @hide */ @@ -105,13 +104,22 @@ public abstract class AppFunctionService extends Service { request, callingPackage, buildCancellationSignal(cancellationCallback), - safeCallback::onResult); + new OutcomeReceiver<>() { + @Override + public void onResult(ExecuteAppFunctionResponse result) { + safeCallback.onResult(result); + } + + @Override + public void onError(AppFunctionException exception) { + safeCallback.onError(exception); + } + }); } catch (Exception ex) { // Apps should handle exceptions. But if they don't, report the error on // behalf of them. - safeCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - getResultCode(ex), ex.getMessage(), /* extras= */ null)); + safeCallback.onError( + new AppFunctionException(toErrorCode(ex), ex.getMessage())); } } }; @@ -164,12 +172,26 @@ public abstract class AppFunctionService extends Service { * @param request The function execution request. * @param callingPackage The package name of the app that is requesting the execution. * @param cancellationSignal A signal to cancel the execution. - * @param callback A callback to report back the result. + * @param callback A callback to report back the result or error. */ @MainThread public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback); + @NonNull + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> + callback); + + /** + * Returns result codes from throwable. + * + * @hide + */ + private static @AppFunctionException.ErrorCode int toErrorCode(@NonNull Throwable t) { + if (t instanceof IllegalArgumentException) { + return AppFunctionException.ERROR_INVALID_ARGUMENT; + } + return AppFunctionException.ERROR_APP_UNKNOWN_ERROR; + } } diff --git a/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java b/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java index a23f842e6eeb..1869d22ea080 100644 --- a/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java +++ b/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java @@ -38,7 +38,7 @@ public class AppFunctionStaticMetadataHelper { public static final String STATIC_SCHEMA_TYPE = "AppFunctionStaticMetadata"; public static final String STATIC_PROPERTY_ENABLED_BY_DEFAULT = "enabledByDefault"; public static final String STATIC_PROPERTY_RESTRICT_CALLERS_WITH_EXECUTE_APP_FUNCTIONS = - "restrictCallersWithExecuteAppFunctions"; + "restrictCallersWithExecuteAppFunctions"; public static final String APP_FUNCTION_STATIC_NAMESPACE = "app_functions"; public static final String PROPERTY_FUNCTION_ID = "functionId"; diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java index 41bb62270e9f..1557815a8468 100644 --- a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java +++ b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java @@ -111,8 +111,8 @@ public final class ExecuteAppFunctionRequest implements Parcelable { * Returns the function parameters. The key is the parameter name, and the value is the * parameter value. * - * <p>The bundle may have missing parameters. Developers are advised to implement defensive - * handling measures. + * <p>The {@link GenericDocument} may have missing parameters. Developers are advised to + * implement defensive handling measures. * * @see AppFunctionManager on how to determine the expected parameters. */ diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java index cdf02e6f5a09..acad43b782e5 100644 --- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java +++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java @@ -19,16 +19,12 @@ package android.app.appfunctions; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; import android.annotation.FlaggedApi; -import android.annotation.IntDef; import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.appsearch.GenericDocument; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** The response to an app function execution. */ @@ -45,10 +41,7 @@ public final class ExecuteAppFunctionResponse implements Parcelable { Bundle extras = Objects.requireNonNull( parcel.readBundle(Bundle.class.getClassLoader())); - int resultCode = parcel.readInt(); - String errorMessage = parcel.readString8(); - return new ExecuteAppFunctionResponse( - resultWrapper, extras, resultCode, errorMessage); + return new ExecuteAppFunctionResponse(resultWrapper.getValue(), extras); } @Override @@ -71,113 +64,7 @@ public final class ExecuteAppFunctionResponse implements Parcelable { * * <p>See {@link #getResultDocument} for more information on extracting the return value. */ - public static final String PROPERTY_RETURN_VALUE = "returnValue"; - - /** - * The call was successful. - * - * <p>This result code does not belong in an error category. - */ - public static final int RESULT_OK = 0; - - /** - * The caller does not have the permission to execute an app function. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_DENIED = 1000; - - /** - * The caller supplied invalid arguments to the execution request. - * - * <p>This error may be considered similar to {@link IllegalArgumentException}. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_INVALID_ARGUMENT = 1001; - - /** - * The caller tried to execute a disabled app function. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_DISABLED = 1002; - - /** - * The caller tried to execute a function that does not exist. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_FUNCTION_NOT_FOUND = 1003; - - /** - * An internal unexpected error coming from the system. - * - * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. - */ - public static final int RESULT_SYSTEM_ERROR = 2000; - - /** - * The operation was cancelled. Use this error code to report that a cancellation is done after - * receiving a cancellation signal. - * - * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. - */ - public static final int RESULT_CANCELLED = 2001; - - /** - * An unknown error occurred while processing the call in the AppFunctionService. - * - * <p>This error is thrown when the service is connected in the remote application but an - * unexpected error is thrown from the bound application. - * - * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. - */ - public static final int RESULT_APP_UNKNOWN_ERROR = 3000; - - /** - * The error category is unknown. - * - * <p>This is the default value for {@link #getErrorCategory}. - */ - public static final int ERROR_CATEGORY_UNKNOWN = 0; - - /** - * The error is caused by the app requesting a function execution. - * - * <p>For example, the caller provided invalid parameters in the execution request e.g. an - * invalid function ID. - * - * <p>Errors in the category fall in the range 1000-1999 inclusive. - */ - public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; - - /** - * The error is caused by an issue in the system. - * - * <p>For example, the AppFunctionService implementation is not found by the system. - * - * <p>Errors in the category fall in the range 2000-2999 inclusive. - */ - public static final int ERROR_CATEGORY_SYSTEM = 2; - - /** - * The error is caused by the app providing the function. - * - * <p>For example, the app crashed when the system is executing the request. - * - * <p>Errors in the category fall in the range 3000-3999 inclusive. - */ - public static final int ERROR_CATEGORY_APP = 3; - - /** The result code of the app function execution. */ - @ResultCode private final int mResultCode; - - /** - * The error message associated with the result, if any. This is {@code null} if the result code - * is {@link #RESULT_OK}. - */ - @Nullable private final String mErrorMessage; + public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue"; /** * Returns the return value of the executed function. @@ -192,103 +79,21 @@ public final class ExecuteAppFunctionResponse implements Parcelable { /** Returns the additional metadata data relevant to this function execution response. */ @NonNull private final Bundle mExtras; - private ExecuteAppFunctionResponse( - @NonNull GenericDocumentWrapper resultDocumentWrapper, - @NonNull Bundle extras, - @ResultCode int resultCode, - @Nullable String errorMessage) { - mResultDocumentWrapper = Objects.requireNonNull(resultDocumentWrapper); - mExtras = Objects.requireNonNull(extras); - mResultCode = resultCode; - mErrorMessage = errorMessage; - } - /** - * Returns result codes from throwable. - * - * @hide - */ - @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) - static @ResultCode int getResultCode(@NonNull Throwable t) { - if (t instanceof IllegalArgumentException) { - return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT; - } - return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR; - } - - /** - * Returns a successful response. - * * @param resultDocument The return value of the executed function. - * @param extras The additional metadata for this function execution response. */ - @NonNull - @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) - public static ExecuteAppFunctionResponse newSuccess( - @NonNull GenericDocument resultDocument, @Nullable Bundle extras) { - Objects.requireNonNull(resultDocument); - Bundle actualExtras = getActualExtras(extras); - GenericDocumentWrapper resultDocumentWrapper = new GenericDocumentWrapper(resultDocument); - - return new ExecuteAppFunctionResponse( - resultDocumentWrapper, actualExtras, RESULT_OK, /* errorMessage= */ null); + public ExecuteAppFunctionResponse(@NonNull GenericDocument resultDocument) { + this(resultDocument, Bundle.EMPTY); } /** - * Returns a failure response. - * - * @param resultCode The result code of the app function execution. + * @param resultDocument The return value of the executed function. * @param extras The additional metadata for this function execution response. - * @param errorMessage The error message associated with the result, if any. - */ - @NonNull - @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) - public static ExecuteAppFunctionResponse newFailure( - @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) { - if (resultCode == RESULT_OK) { - throw new IllegalArgumentException("resultCode must not be RESULT_OK"); - } - Bundle actualExtras = getActualExtras(extras); - GenericDocumentWrapper emptyWrapper = - new GenericDocumentWrapper(new GenericDocument.Builder<>("", "", "").build()); - return new ExecuteAppFunctionResponse(emptyWrapper, actualExtras, resultCode, errorMessage); - } - - private static Bundle getActualExtras(@Nullable Bundle extras) { - if (extras == null) { - return Bundle.EMPTY; - } - return extras; - } - - /** - * Returns the error category of the {@link ExecuteAppFunctionResponse}. - * - * <p>This method categorizes errors based on their underlying cause, allowing developers to - * implement targeted error handling and provide more informative error messages to users. It - * maps ranges of result codes to specific error categories. - * - * <p>When constructing a {@link #newFailure} response, use the appropriate result code value to - * ensure correct categorization of the failed response. - * - * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to - * any error category, for example, in the case of a successful result with {@link #RESULT_OK}. - * - * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding - * result code ranges. */ - @ErrorCategory - public int getErrorCategory() { - if (mResultCode >= 1000 && mResultCode < 2000) { - return ERROR_CATEGORY_REQUEST_ERROR; - } - if (mResultCode >= 2000 && mResultCode < 3000) { - return ERROR_CATEGORY_SYSTEM; - } - if (mResultCode >= 3000 && mResultCode < 4000) { - return ERROR_CATEGORY_APP; - } - return ERROR_CATEGORY_UNKNOWN; + public ExecuteAppFunctionResponse( + @NonNull GenericDocument resultDocument, @NonNull Bundle extras) { + mResultDocumentWrapper = new GenericDocumentWrapper(Objects.requireNonNull(resultDocument)); + mExtras = Objects.requireNonNull(extras); } /** @@ -296,9 +101,6 @@ public final class ExecuteAppFunctionResponse implements Parcelable { * * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. * - * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed - * function does not produce a return value. - * * <p>Sample code for extracting the return value: * * <pre> @@ -324,32 +126,6 @@ public final class ExecuteAppFunctionResponse implements Parcelable { return mExtras; } - /** - * Returns {@code true} if {@link #getResultCode} equals {@link - * ExecuteAppFunctionResponse#RESULT_OK}. - */ - public boolean isSuccess() { - return getResultCode() == RESULT_OK; - } - - /** - * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}. - */ - @ResultCode - public int getResultCode() { - return mResultCode; - } - - /** - * Returns the error message associated with this result. - * - * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. - */ - @Nullable - public String getErrorMessage() { - return mErrorMessage; - } - @Override public int describeContents() { return 0; @@ -359,43 +135,5 @@ public final class ExecuteAppFunctionResponse implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { mResultDocumentWrapper.writeToParcel(dest, flags); dest.writeBundle(mExtras); - dest.writeInt(mResultCode); - dest.writeString8(mErrorMessage); } - - /** - * Result codes. - * - * @hide - */ - @IntDef( - prefix = {"RESULT_"}, - value = { - RESULT_OK, - RESULT_DENIED, - RESULT_APP_UNKNOWN_ERROR, - RESULT_FUNCTION_NOT_FOUND, - RESULT_SYSTEM_ERROR, - RESULT_INVALID_ARGUMENT, - RESULT_DISABLED, - RESULT_CANCELLED - }) - @Retention(RetentionPolicy.SOURCE) - public @interface ResultCode {} - - /** - * Error categories. - * - * @hide - */ - @IntDef( - prefix = {"ERROR_CATEGORY_"}, - value = { - ERROR_CATEGORY_UNKNOWN, - ERROR_CATEGORY_REQUEST_ERROR, - ERROR_CATEGORY_APP, - ERROR_CATEGORY_SYSTEM - }) - @Retention(RetentionPolicy.SOURCE) - public @interface ErrorCategory {} } diff --git a/core/java/android/app/appfunctions/GenericDocumentWrapper.java b/core/java/android/app/appfunctions/GenericDocumentWrapper.java index b29b64e44d21..541ca7458efe 100644 --- a/core/java/android/app/appfunctions/GenericDocumentWrapper.java +++ b/core/java/android/app/appfunctions/GenericDocumentWrapper.java @@ -34,9 +34,9 @@ import java.util.Objects; * <p>{#link {@link Parcel#writeBlob(byte[])}} could take care of whether to pass data via binder * directly or Android shared memory if the data is large. * - * <p>This class performs lazy unparcelling. The `GenericDocument` is only unparcelled - * from the underlying `Parcel` when {@link #getValue()} is called. This optimization - * allows the system server to pass through the generic document, without unparcel and parcel it. + * <p>This class performs lazy unparcelling. The `GenericDocument` is only unparcelled from the + * underlying `Parcel` when {@link #getValue()} is called. This optimization allows the system + * server to pass through the generic document, without unparcel and parcel it. * * @hide * @see Parcel#writeBlob(byte[]) @@ -45,8 +45,11 @@ public final class GenericDocumentWrapper implements Parcelable { @Nullable @GuardedBy("mLock") private GenericDocument mGenericDocument; + @GuardedBy("mLock") - @Nullable private Parcel mParcel; + @Nullable + private Parcel mParcel; + private final Object mLock = new Object(); public static final Creator<GenericDocumentWrapper> CREATOR = diff --git a/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl b/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl index 5323f9b627e3..69bbc0e5d275 100644 --- a/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl +++ b/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl @@ -17,8 +17,10 @@ package android.app.appfunctions; import android.app.appfunctions.ExecuteAppFunctionResponse; +import android.app.appfunctions.AppFunctionException; /** {@hide} */ oneway interface IExecuteAppFunctionCallback { - void onResult(in ExecuteAppFunctionResponse result); + void onSuccess(in ExecuteAppFunctionResponse result); + void onError(in AppFunctionException exception); } diff --git a/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java b/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java index 00182447e9a8..2426daf5c9f2 100644 --- a/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java +++ b/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java @@ -17,17 +17,16 @@ package android.app.appfunctions; import android.annotation.NonNull; -import android.annotation.Nullable; import android.os.RemoteException; import android.util.Log; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; /** * A wrapper of IExecuteAppFunctionCallback which swallows the {@link RemoteException}. This - * callback is intended for one-time use only. Subsequent calls to onResult() will be ignored. + * callback is intended for one-time use only. Subsequent calls to onResult() or onError() will be + * ignored. * * @hide */ @@ -38,44 +37,41 @@ public class SafeOneTimeExecuteAppFunctionCallback { @NonNull private final IExecuteAppFunctionCallback mCallback; - @Nullable private final Consumer<ExecuteAppFunctionResponse> mOnDispatchCallback; - public SafeOneTimeExecuteAppFunctionCallback(@NonNull IExecuteAppFunctionCallback callback) { - this(callback, /* onDispatchCallback= */ null); - } - - /** - * @param callback The callback to wrap. - * @param onDispatchCallback An optional callback invoked after the wrapped callback has been - * dispatched with a result. This callback receives the result that has been dispatched. - */ - public SafeOneTimeExecuteAppFunctionCallback( - @NonNull IExecuteAppFunctionCallback callback, - @Nullable Consumer<ExecuteAppFunctionResponse> onDispatchCallback) { mCallback = Objects.requireNonNull(callback); - mOnDispatchCallback = onDispatchCallback; } /** Invoke wrapped callback with the result. */ public void onResult(@NonNull ExecuteAppFunctionResponse result) { if (!mOnResultCalled.compareAndSet(false, true)) { - Log.w(TAG, "Ignore subsequent calls to onResult()"); + Log.w(TAG, "Ignore subsequent calls to onResult/onError()"); return; } try { - mCallback.onResult(result); + mCallback.onSuccess(result); } catch (RemoteException ex) { // Failed to notify the other end. Ignore. Log.w(TAG, "Failed to invoke the callback", ex); } - if (mOnDispatchCallback != null) { - mOnDispatchCallback.accept(result); + } + + /** Invoke wrapped callback with the error. */ + public void onError(@NonNull AppFunctionException error) { + if (!mOnResultCalled.compareAndSet(false, true)) { + Log.w(TAG, "Ignore subsequent calls to onResult/onError()"); + return; + } + try { + mCallback.onError(error); + } catch (RemoteException ex) { + // Failed to notify the other end. Ignore. + Log.w(TAG, "Failed to invoke the callback", ex); } } /** - * Disables this callback. Subsequent calls to {@link #onResult(ExecuteAppFunctionResponse)} - * will be ignored. + * Disables this callback. Subsequent calls to {@link #onResult(ExecuteAppFunctionResponse)} or + * {@link #onError(AppFunctionException)} will be ignored. */ public void disable() { mOnResultCalled.set(true); diff --git a/core/java/android/app/assist/AssistContent.java b/core/java/android/app/assist/AssistContent.java index a48868906487..43a46ba7885d 100644 --- a/core/java/android/app/assist/AssistContent.java +++ b/core/java/android/app/assist/AssistContent.java @@ -1,5 +1,6 @@ package android.app.assist; +import android.annotation.FlaggedApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipData; import android.content.Intent; @@ -15,6 +16,20 @@ import android.os.Parcelable; * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}. */ public class AssistContent implements Parcelable { + /** + * Extra for a {@link Bundle} that provides contextual AppFunction's information about the + * content currently being viewed in the application. + * <p> + * This extra can be optionally supplied in the {@link AssistContent#getExtras()} bundle. + * <p> + * The schema of the {@link Bundle} in this extra is defined in the AppFunction SDK. + * + * @see android.app.appfunctions.AppFunctionManager + */ + @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) + public static final String EXTRA_APP_FUNCTION_DATA = + "android.app.assist.extra.APP_FUNCTION_DATA"; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private boolean mIsAppProvidedIntent = false; private boolean mIsAppProvidedWebUri = false; diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig index 9a645192a155..c8455c1f439f 100644 --- a/core/java/android/app/multitasking.aconfig +++ b/core/java/android/app/multitasking.aconfig @@ -8,3 +8,11 @@ flag { description: "Enables PiP UI state callback on entering" bug: "303718131" } + +flag { + name: "enable_tv_implicit_enter_pip_restriction" + is_exported: true + namespace: "tv_system_ui" + description: "Enables restrictions to PiP entry on TV for setAutoEnterEnabled and lifecycle methods" + bug: "283115999" +} diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index ee93870be055..6934e9883840 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -100,16 +100,6 @@ flag { } flag { - name: "visit_person_uri" - namespace: "systemui" - description: "Guards the security fix that ensures all URIs Person.java are valid" - bug: "281044385" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notification_expansion_optional" namespace: "systemui" description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions." diff --git a/core/java/android/app/performance.aconfig b/core/java/android/app/performance.aconfig index f51f748bc71f..61b53f97fea1 100644 --- a/core/java/android/app/performance.aconfig +++ b/core/java/android/app/performance.aconfig @@ -18,3 +18,20 @@ flag { description: "Enforce PropertyInvalidatedCache.setTestMode() protocol" bug: "360897450" } + +flag { + namespace: "system_performance" + name: "pic_isolate_cache_by_uid" + is_fixed_read_only: true + description: "Ensure that different UIDs use different caches" + bug: "373752556" +} + +flag { + namespace: "system_performance" + name: "pic_isolated_cache_statistics" + is_fixed_read_only: true + description: "Collects statistics for cache UID isolation strategies" + bug: "373752556" +} + diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig index bcb5b3636c95..d5e696d49ff4 100644 --- a/core/java/android/app/supervision/flags.aconfig +++ b/core/java/android/app/supervision/flags.aconfig @@ -7,4 +7,12 @@ flag { namespace: "supervision" description: "Flag to enable the SupervisionService" bug: "340351729" -}
\ No newline at end of file +} + +flag { + name: "supervision_api_on_wear" + is_exported: true + namespace: "supervision" + description: "Flag to enable the SupervisionService on Wear devices" + bug: "373358935" +} diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig index 4b880d030413..32334d893d75 100644 --- a/core/java/android/app/wallpaper.aconfig +++ b/core/java/android/app/wallpaper.aconfig @@ -22,3 +22,11 @@ flag { bug: "347235611" is_exported: true } + +flag { + name: "customization_packs_apis" + is_exported: true + namespace: "systemui" + description: "Move APIs related to bitmap and crops to @SystemApi." + bug: "372344184" +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 186f7b3e111c..6086f2455a31 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -6802,6 +6802,12 @@ public abstract class Context { public static final String MEDIA_QUALITY_SERVICE = "media_quality"; /** + * Service to perform operations needed for dynamic instrumentation. + * @hide + */ + public static final String DYNAMIC_INSTRUMENTATION_SERVICE = "dynamic_instrumentation"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 34c3f5798bc5..e9e8578af787 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -38,6 +38,7 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AttributeSet; +import android.util.EmptyArray; import android.util.Pair; import android.util.Slog; @@ -565,10 +566,7 @@ public class ApkLiteParseUtils { usesSdkLibrariesVersionsMajor, usesSdkLibVersionMajor, /*allowDuplicates=*/ true); - // We allow ":" delimiters in the SHA declaration as this is the format - // emitted by the certtool making it easy for developers to copy/paste. - // TODO(372862145): Add test for this replacement - usesSdkCertDigest = usesSdkCertDigest.replace(":", "").toLowerCase(); + usesSdkCertDigest = normalizeCertDigest(usesSdkCertDigest); if ("".equals(usesSdkCertDigest)) { // Test-only uses-sdk-library empty certificate digest override. @@ -618,18 +616,23 @@ public class ApkLiteParseUtils { usesStaticLibrariesVersions, usesStaticLibVersion, /*allowDuplicates=*/ true); - // We allow ":" delimiters in the SHA declaration as this is the format - // emitted by the certtool making it easy for developers to copy/paste. - // TODO(372862145): Add test for this replacement - usesStaticLibCertDigest = - usesStaticLibCertDigest.replace(":", "").toLowerCase(); + usesStaticLibCertDigest = normalizeCertDigest(usesStaticLibCertDigest); + + ParseResult<String[]> certResult = + parseAdditionalCertificates(input, parser); + if (certResult.isError()) { + return input.error(certResult); + } + String[] additionalCertSha256Digests = certResult.getResult(); + String[] certSha256Digests = + new String[additionalCertSha256Digests.length + 1]; + certSha256Digests[0] = usesStaticLibCertDigest; + System.arraycopy(additionalCertSha256Digests, 0, certSha256Digests, + 1, additionalCertSha256Digests.length); - // TODO(372862145): Add support for multiple signer for app targeting - // O-MR1 usesStaticLibrariesCertDigests = ArrayUtils.appendElement( String[].class, usesStaticLibrariesCertDigests, - new String[]{usesStaticLibCertDigest}, - /*allowDuplicates=*/ true); + certSha256Digests, /*allowDuplicates=*/ true); break; case TAG_SDK_LIBRARY: isSdkLibrary = true; @@ -809,6 +812,43 @@ public class ApkLiteParseUtils { declaredLibraries)); } + private static ParseResult<String[]> parseAdditionalCertificates(ParseInput input, + XmlResourceParser parser) throws XmlPullParserException, IOException { + String[] certSha256Digests = EmptyArray.STRING; + final int depth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > depth)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final String nodeName = parser.getName(); + if (nodeName.equals("additional-certificate")) { + String certSha256Digest = parser.getAttributeValue( + ANDROID_RES_NAMESPACE, "certDigest"); + if (TextUtils.isEmpty(certSha256Digest)) { + return input.error("Bad additional-certificate declaration with empty" + + " certDigest:" + certSha256Digest); + } + + certSha256Digest = normalizeCertDigest(certSha256Digest); + certSha256Digests = ArrayUtils.appendElement(String.class, + certSha256Digests, certSha256Digest); + } + } + + return input.success(certSha256Digests); + } + + /** + * We allow ":" delimiters in the SHA declaration as this is the format emitted by the + * certtool making it easy for developers to copy/paste. + */ + private static String normalizeCertDigest(String certDigest) { + return certDigest.replace(":", "").toLowerCase(); + } + private static boolean isDeviceAdminReceiver( XmlResourceParser parser, boolean applicationHasBindDeviceAdminPermission) throws XmlPullParserException, IOException { diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index 26ecbd1982d5..f23c193e2da0 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -95,3 +95,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "layout_readwrite_flags" + is_exported: true + namespace: "resource_manager" + description: "Feature flag for allowing read/write flags in layout files" + bug: "377974898" + # This flag is used to control aapt2 behavior. + is_fixed_read_only: true +} diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index ef59e0af3a27..93ef5c365730 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -45,7 +45,7 @@ import dalvik.system.CloseGuard; * </p> */ @RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("CursorWindow_host") +@RavenwoodRedirectionClass("CursorWindow_ravenwood") public class CursorWindow extends SQLiteClosable implements Parcelable { private static final String STATS_TAG = "CursorWindowStats"; diff --git a/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java b/core/java/android/database/CursorWindow_ravenwood.java index e21a9cd71a2d..990ec5e9d59e 100644 --- a/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java +++ b/core/java/android/database/CursorWindow_ravenwood.java @@ -17,6 +17,7 @@ package android.database; import android.database.sqlite.SQLiteException; import android.os.Parcel; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.util.Base64; import java.text.DecimalFormat; @@ -26,9 +27,10 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; -public class CursorWindow_host { +@RavenwoodKeepWholeClass +class CursorWindow_ravenwood { - private static final HashMap<Long, CursorWindow_host> sInstances = new HashMap<>(); + private static final HashMap<Long, CursorWindow_ravenwood> sInstances = new HashMap<>(); private static long sNextId = 1; private String mName; @@ -41,7 +43,7 @@ public class CursorWindow_host { private final List<Row> mRows = new ArrayList<>(); public static long nativeCreate(String name, int cursorWindowSize) { - CursorWindow_host instance = new CursorWindow_host(); + CursorWindow_ravenwood instance = new CursorWindow_ravenwood(); instance.mName = name; long instanceId = sNextId++; sInstances.put(instanceId, instance); @@ -66,7 +68,7 @@ public class CursorWindow_host { } public static boolean nativeAllocRow(long windowPtr) { - CursorWindow_host instance = sInstances.get(windowPtr); + CursorWindow_ravenwood instance = sInstances.get(windowPtr); Row row = new Row(); row.mFields = new String[instance.mColumnNum]; row.mTypes = new int[instance.mColumnNum]; @@ -76,7 +78,7 @@ public class CursorWindow_host { } private static boolean put(long windowPtr, String value, int type, int row, int column) { - CursorWindow_host instance = sInstances.get(windowPtr); + CursorWindow_ravenwood instance = sInstances.get(windowPtr); if (row >= instance.mRows.size() || column >= instance.mColumnNum) { return false; } @@ -87,7 +89,7 @@ public class CursorWindow_host { } public static int nativeGetType(long windowPtr, int row, int column) { - CursorWindow_host instance = sInstances.get(windowPtr); + CursorWindow_ravenwood instance = sInstances.get(windowPtr); if (row >= instance.mRows.size() || column >= instance.mColumnNum) { return Cursor.FIELD_TYPE_NULL; } @@ -101,7 +103,7 @@ public class CursorWindow_host { } public static String nativeGetString(long windowPtr, int row, int column) { - CursorWindow_host instance = sInstances.get(windowPtr); + CursorWindow_ravenwood instance = sInstances.get(windowPtr); if (row >= instance.mRows.size() || column >= instance.mColumnNum) { return null; } @@ -164,7 +166,7 @@ public class CursorWindow_host { } public static void nativeWriteToParcel(long windowPtr, Parcel parcel) { - CursorWindow_host window = sInstances.get(windowPtr); + CursorWindow_ravenwood window = sInstances.get(windowPtr); parcel.writeString(window.mName); parcel.writeInt(window.mColumnNum); parcel.writeInt(window.mRows.size()); @@ -176,7 +178,7 @@ public class CursorWindow_host { public static long nativeCreateFromParcel(Parcel parcel) { long windowPtr = nativeCreate(null, 0); - CursorWindow_host window = sInstances.get(windowPtr); + CursorWindow_ravenwood window = sInstances.get(windowPtr); window.mName = parcel.readString(); window.mColumnNum = parcel.readInt(); int rowCount = parcel.readInt(); diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index b7856303fc05..75e20582b7b4 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -994,7 +994,7 @@ public final class CameraManager { AttributionSourceState contextAttributionSourceState = contextAttributionSource.asState(); - if (Flags.useContextAttributionSource() && useContextAttributionSource) { + if (Flags.dataDeliveryPermissionChecks() && useContextAttributionSource) { return contextAttributionSourceState; } else { AttributionSourceState clientAttribution = diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 987e2ad768b0..2f6c6a3b02d8 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -3371,6 +3371,32 @@ public abstract class CameraMetadata<TKey> { public static final int CONTROL_AUTOFRAMING_AUTO = 2; // + // Enumeration values for CaptureRequest#CONTROL_ZOOM_METHOD + // + + /** + * <p>The camera device automatically detects whether the application does zoom with + * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and in turn decides which + * metadata tag reflects the effective zoom level.</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CaptureRequest#SCALER_CROP_REGION + * @see CaptureRequest#CONTROL_ZOOM_METHOD + */ + @FlaggedApi(Flags.FLAG_ZOOM_METHOD) + public static final int CONTROL_ZOOM_METHOD_AUTO = 0; + + /** + * <p>The application intends to control zoom via {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and + * the effective zoom level is reflected by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results.</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CaptureRequest#CONTROL_ZOOM_METHOD + */ + @FlaggedApi(Flags.FLAG_ZOOM_METHOD) + public static final int CONTROL_ZOOM_METHOD_ZOOM_RATIO = 1; + + // // Enumeration values for CaptureRequest#EDGE_MODE // diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 8142bbe9b838..9846cac55212 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -21,7 +21,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.hardware.camera2.impl.CameraMetadataNative; -import android.hardware.camera2.impl.ExtensionKey; import android.hardware.camera2.impl.PublicKey; import android.hardware.camera2.impl.SyntheticKey; import android.hardware.camera2.params.OutputConfiguration; @@ -2668,6 +2667,45 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> new Key<Integer>("android.control.autoframing", int.class); /** + * <p>Whether the application uses {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} + * to control zoom levels.</p> + * <p>If set to AUTO, the camera device detects which capture request key the application uses + * to do zoom, {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. If + * the application doesn't set android.scaler.zoomRatio or sets it to 1.0 in the capture + * request, the effective zoom level is reflected in {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} in capture + * results. If {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} is set to values other than 1.0, the effective + * zoom level is reflected in {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. AUTO is the default value + * for this control, and also the behavior of the OS before Android version + * {@link android.os.Build.VERSION_CODES#BAKLAVA BAKLAVA}.</p> + * <p>If set to ZOOM_RATIO, the application explicitly specifies zoom level be controlled + * by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and the effective zoom level is reflected in + * {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results. This addresses an ambiguity with AUTO, + * with which the camera device cannot know if the application is using cropRegion or + * zoomRatio at 1.0x.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_ZOOM_METHOD_AUTO AUTO}</li> + * <li>{@link #CONTROL_ZOOM_METHOD_ZOOM_RATIO ZOOM_RATIO}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SCALER_CROP_REGION + * @see #CONTROL_ZOOM_METHOD_AUTO + * @see #CONTROL_ZOOM_METHOD_ZOOM_RATIO + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_ZOOM_METHOD) + public static final Key<Integer> CONTROL_ZOOM_METHOD = + new Key<Integer>("android.control.zoomMethod", int.class); + + /** * <p>Operation mode for edge * enhancement.</p> * <p>Edge enhancement improves sharpness and details in the captured image. OFF means diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index bf3a072ff097..674fc662aeac 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -22,7 +22,6 @@ import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.CaptureResultExtras; -import android.hardware.camera2.impl.ExtensionKey; import android.hardware.camera2.impl.PublicKey; import android.hardware.camera2.impl.SyntheticKey; import android.hardware.camera2.utils.TypeReference; @@ -2915,6 +2914,45 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { new Key<Integer>("android.control.lowLightBoostState", int.class); /** + * <p>Whether the application uses {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} + * to control zoom levels.</p> + * <p>If set to AUTO, the camera device detects which capture request key the application uses + * to do zoom, {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. If + * the application doesn't set android.scaler.zoomRatio or sets it to 1.0 in the capture + * request, the effective zoom level is reflected in {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} in capture + * results. If {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} is set to values other than 1.0, the effective + * zoom level is reflected in {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. AUTO is the default value + * for this control, and also the behavior of the OS before Android version + * {@link android.os.Build.VERSION_CODES#BAKLAVA BAKLAVA}.</p> + * <p>If set to ZOOM_RATIO, the application explicitly specifies zoom level be controlled + * by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and the effective zoom level is reflected in + * {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results. This addresses an ambiguity with AUTO, + * with which the camera device cannot know if the application is using cropRegion or + * zoomRatio at 1.0x.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_ZOOM_METHOD_AUTO AUTO}</li> + * <li>{@link #CONTROL_ZOOM_METHOD_ZOOM_RATIO ZOOM_RATIO}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SCALER_CROP_REGION + * @see #CONTROL_ZOOM_METHOD_AUTO + * @see #CONTROL_ZOOM_METHOD_ZOOM_RATIO + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_ZOOM_METHOD) + public static final Key<Integer> CONTROL_ZOOM_METHOD = + new Key<Integer>("android.control.zoomMethod", int.class); + + /** * <p>Operation mode for edge * enhancement.</p> * <p>Edge enhancement improves sharpness and details in the captured image. OFF means diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java index e22c263e893d..1cc085658bfa 100644 --- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java +++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java @@ -437,7 +437,7 @@ public class CameraMetadataNative implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { + public synchronized void writeToParcel(Parcel dest, int flags) { nativeWriteToParcel(dest, mMetadataPtr); } @@ -479,7 +479,7 @@ public class CameraMetadataNative implements Parcelable { return getBase(key); } - public void readFromParcel(Parcel in) { + public synchronized void readFromParcel(Parcel in) { nativeReadFromParcel(in, mMetadataPtr); updateNativeAllocation(); } @@ -592,28 +592,33 @@ public class CameraMetadataNative implements Parcelable { } private <T> T getBase(Key<T> key) { - int tag; - if (key.hasTag()) { - tag = key.getTag(); - } else { - tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.getName()); - key.cacheTag(tag); - } - byte[] values = readValues(tag); - if (values == null) { - // If the key returns null, use the fallback key if exists. - // This is to support old key names for the newly published keys. - if (key.mFallbackName == null) { - return null; + int tag, nativeType; + byte[] values = null; + synchronized (this) { + if (key.hasTag()) { + tag = key.getTag(); + } else { + tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.getName()); + key.cacheTag(tag); } - tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.mFallbackName); values = readValues(tag); if (values == null) { - return null; + // If the key returns null, use the fallback key if exists. + // This is to support old key names for the newly published keys. + if (key.mFallbackName == null) { + return null; + } + tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.mFallbackName); + values = readValues(tag); + if (values == null) { + return null; + } } - } - int nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag); + nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag); + } + // This block of code doesn't need to be synchronized since we aren't writing or reading + // from the metadata buffer for this instance of CameraMetadataNative. Marshaler<T> marshaler = getMarshalerForKey(key, nativeType); ByteBuffer buffer = ByteBuffer.wrap(values).order(ByteOrder.nativeOrder()); return marshaler.unmarshal(buffer); @@ -1945,8 +1950,12 @@ public class CameraMetadataNative implements Parcelable { setBase(key.getNativeKey(), value); } - private <T> void setBase(Key<T> key, T value) { - int tag; + // The whole method needs to be synchronized since we're making + // multiple calls to the native layer. From one call to the other (within setBase) + // we expect the metadata's properties such as vendor id etc to + // stay the same and as a result the whole method should be synchronized for safety. + private synchronized <T> void setBase(Key<T> key, T value) { + int tag, nativeType; if (key.hasTag()) { tag = key.getTag(); } else { @@ -1959,7 +1968,7 @@ public class CameraMetadataNative implements Parcelable { return; } // else update the entry to a new value - int nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag); + nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag); Marshaler<T> marshaler = getMarshalerForKey(key, nativeType); int size = marshaler.calculateMarshalSize(value); @@ -2162,7 +2171,7 @@ public class CameraMetadataNative implements Parcelable { return true; } - private void updateNativeAllocation() { + private synchronized void updateNativeAllocation() { long currentBufferSize = nativeGetBufferSize(mMetadataPtr); if (currentBufferSize != mBufferSize) { @@ -2245,6 +2254,11 @@ public class CameraMetadataNative implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private long mMetadataPtr; // native std::shared_ptr<CameraMetadata>* + // FastNative doesn't work with synchronized methods and we can do synchronization + // wherever needed in the java layer (caller). At some places in java such as + // setBase() / getBase(), we do need to synchronize the whole method, so leaving + // synchronized out for these native methods. + @FastNative private static native long nativeAllocate(); @FastNative @@ -2254,28 +2268,41 @@ public class CameraMetadataNative implements Parcelable { @FastNative private static native void nativeUpdate(long dst, long src); - private static synchronized native void nativeWriteToParcel(Parcel dest, long ptr); - private static synchronized native void nativeReadFromParcel(Parcel source, long ptr); - private static synchronized native void nativeSwap(long ptr, long otherPtr) + @FastNative + private static native void nativeWriteToParcel(Parcel dest, long ptr); + @FastNative + private static native void nativeReadFromParcel(Parcel source, long ptr); + @FastNative + private static native void nativeSwap(long ptr, long otherPtr) throws NullPointerException; @FastNative private static native void nativeSetVendorId(long ptr, long vendorId); - private static synchronized native void nativeClose(long ptr); - private static synchronized native boolean nativeIsEmpty(long ptr); - private static synchronized native int nativeGetEntryCount(long ptr); - private static synchronized native long nativeGetBufferSize(long ptr); + @FastNative + private static native void nativeClose(long ptr); + @FastNative + private static native boolean nativeIsEmpty(long ptr); + @FastNative + private static native int nativeGetEntryCount(long ptr); + @FastNative + private static native long nativeGetBufferSize(long ptr); @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private static synchronized native byte[] nativeReadValues(int tag, long ptr); - private static synchronized native void nativeWriteValues(int tag, byte[] src, long ptr); - private static synchronized native void nativeDump(long ptr) throws IOException; // dump to LOGD + @FastNative + private static native byte[] nativeReadValues(int tag, long ptr); + @FastNative + private static native void nativeWriteValues(int tag, byte[] src, long ptr); + @FastNative + private static native void nativeDump(long ptr) throws IOException; // dump to LOGD - private static synchronized native ArrayList nativeGetAllVendorKeys(long ptr, Class keyClass); + @FastNative + private static native ArrayList nativeGetAllVendorKeys(long ptr, Class keyClass); @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private static synchronized native int nativeGetTagFromKeyLocal(long ptr, String keyName) + @FastNative + private static native int nativeGetTagFromKeyLocal(long ptr, String keyName) throws IllegalArgumentException; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private static synchronized native int nativeGetTypeFromTagLocal(long ptr, int tag) + @FastNative + private static native int nativeGetTypeFromTagLocal(long ptr, int tag) throws IllegalArgumentException; @FastNative private static native int nativeGetTagFromKey(String keyName, long vendorId) @@ -2293,7 +2320,7 @@ public class CameraMetadataNative implements Parcelable { * @throws NullPointerException if other was null * @hide */ - public void swap(CameraMetadataNative other) { + public synchronized void swap(CameraMetadataNative other) { nativeSwap(mMetadataPtr, other.mMetadataPtr); mCameraId = other.mCameraId; mHasMandatoryConcurrentStreams = other.mHasMandatoryConcurrentStreams; @@ -2308,14 +2335,14 @@ public class CameraMetadataNative implements Parcelable { * * @hide */ - public void setVendorId(long vendorId) { + public synchronized void setVendorId(long vendorId) { nativeSetVendorId(mMetadataPtr, vendorId); } /** * @hide */ - public int getEntryCount() { + public synchronized int getEntryCount() { return nativeGetEntryCount(mMetadataPtr); } @@ -2324,7 +2351,7 @@ public class CameraMetadataNative implements Parcelable { * * @hide */ - public boolean isEmpty() { + public synchronized boolean isEmpty() { return nativeIsEmpty(mMetadataPtr); } @@ -2343,7 +2370,7 @@ public class CameraMetadataNative implements Parcelable { * * @hide */ - public <K> ArrayList<K> getAllVendorKeys(Class<K> keyClass) { + public synchronized <K> ArrayList<K> getAllVendorKeys(Class<K> keyClass) { if (keyClass == null) { throw new NullPointerException(); } @@ -2398,7 +2425,7 @@ public class CameraMetadataNative implements Parcelable { * * @hide */ - public void writeValues(int tag, byte[] src) { + public synchronized void writeValues(int tag, byte[] src) { nativeWriteValues(tag, src, mMetadataPtr); } @@ -2413,7 +2440,7 @@ public class CameraMetadataNative implements Parcelable { * @return {@code null} if there were 0 entries for this tag, a byte[] otherwise. * @hide */ - public byte[] readValues(int tag) { + public synchronized byte[] readValues(int tag) { // TODO: Optimization. Native code returns a ByteBuffer instead. return nativeReadValues(tag, mMetadataPtr); } @@ -2426,7 +2453,7 @@ public class CameraMetadataNative implements Parcelable { * * @hide */ - public void dumpToLog() { + public synchronized void dumpToLog() { try { nativeDump(mMetadataPtr); } catch (IOException e) { diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 9d42b67bf5a8..24951c4d516e 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -117,6 +117,10 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW = 69; public static final int KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW = 70; public static final int KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE = 71; + public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN = 72; + public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT = 73; + public static final int KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION = 74; + public static final int KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK = 75; public static final int FLAG_CANCELLED = 1; @@ -203,6 +207,10 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW, KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE, + KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN, + KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT, + KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyGestureType { @@ -773,6 +781,14 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW"; case KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE: return "KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE"; + case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN: + return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN"; + case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT: + return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT"; + case KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION: + return "KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION"; + case KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK: + return "KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK"; default: return Integer.toHexString(value); } diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 38e32c61c99e..4b2f2c218e5a 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -30,14 +30,6 @@ flag { flag { namespace: "input_native" - name: "emoji_and_screenshot_keycodes_available" - is_exported: true - description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots" - bug: "315307777" -} - -flag { - namespace: "input_native" name: "keyboard_a11y_slow_keys_flag" description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user" bug: "294546335" @@ -153,9 +145,16 @@ flag { } flag { + name: "enable_new_25q2_keycodes" + namespace: "input" + description: "Enables new 25Q2 keycodes" + bug: "365920375" +} + +flag { name: "override_power_key_behavior_in_focused_window" - namespace: "input_native" - description: "Allows privileged focused windows to capture power key events." + namespace: "wallet_integration" + description: "Allows privileged focused windows to override the power key double tap behavior." bug: "357144512" } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 8c3f0ef08039..ae8366817f2b 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -55,6 +55,7 @@ import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECT import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_OTHER; import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED; import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING; +import static android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API; import static android.view.inputmethod.Flags.ctrlShiftShortcut; import static android.view.inputmethod.Flags.predictiveBackIme; @@ -4392,6 +4393,39 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Called when the requested visibility of a custom IME Switcher button changes. + * + * <p>When the system provides an IME navigation bar, it may decide to show an IME Switcher + * button inside this bar. However, the IME can request hiding the bar provided by the system + * with {@code getWindowInsetsController().hide(captionBar())} (the IME navigation bar provides + * {@link Type#captionBar() captionBar} insets to the IME window). If the request is successful, + * then it becomes the IME's responsibility to provide a custom IME Switcher button in its + * input view, with equivalent functionality.</p> + * + * <p>This custom button is only requested to be visible when the system provides the IME + * navigation bar, both the bar and the IME Switcher button inside it should be visible, + * but the IME successfully requested to hide the bar. This does not depend on the current + * visibility of the IME. It could be called with {@code true} while the IME is hidden, in + * which case the IME should prepare to show the button as soon as the IME itself is shown.</p> + * + * <p>This is only called when the requested visibility changes. The default value is + * {@code false} and as such, this will not be called initially if the resulting value is + * {@code false}.</p> + * + * <p>This can be called at any time after {@link #onCreate}, even if the IME is not currently + * visible. However, this is not guaranteed to be called before the IME is shown, as it depends + * on when the IME requested hiding the IME navigation bar. If the request is sent during + * the showing flow (e.g. during {@link #onStartInputView}), this will be called shortly after + * {@link #onWindowShown}, but before the first IME frame is drawn.</p> + * + * @param visible whether the button is requested visible or not. + */ + @FlaggedApi(FLAG_IME_SWITCHER_REVAMP_API) + public void onCustomImeSwitcherButtonRequestedVisible(boolean visible) { + // Intentionally empty + } + + /** * Called when the IME switch button was clicked from the client. Depending on the number of * enabled IME subtypes, this will either switch to the next IME/subtype, or show the input * method picker dialog. diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index b08454dd7f8f..38be8d9f772d 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -41,6 +41,7 @@ import android.view.WindowInsets; import android.view.WindowInsetsController.Appearance; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; @@ -178,6 +179,9 @@ final class NavigationBarController { private boolean mDrawLegacyNavigationBarBackground; + /** Whether a custom IME Switcher button should be visible. */ + private boolean mCustomImeSwitcherVisible; + private final Rect mTempRect = new Rect(); private final int[] mTempPos = new int[2]; @@ -265,6 +269,7 @@ final class NavigationBarController { // IME navigation bar. boolean visible = insets.isVisible(captionBar()); mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE); + checkCustomImeSwitcherVisibility(); } return view.onApplyWindowInsets(insets); }); @@ -491,6 +496,8 @@ final class NavigationBarController { mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; + checkCustomImeSwitcherVisibility(); + mService.mWindow.getWindow().getDecorView().getWindowInsetsController() .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight(imeDrawsImeNavBar)); @@ -616,12 +623,33 @@ final class NavigationBarController { && mNavigationBarFrame.getVisibility() == View.VISIBLE; } + /** + * Checks if a custom IME Switcher button should be visible, and notifies the IME when this + * state changes. This can only be {@code true} if three conditions are met: + * + * <li>The IME should draw the IME navigation bar.</li> + * <li>The IME Switcher button should be visible when the IME is visible.</li> + * <li>The IME navigation bar should be visible, but was requested hidden by the IME.</li> + */ + private void checkCustomImeSwitcherVisibility() { + if (!Flags.imeSwitcherRevampApi()) { + return; + } + final boolean visible = mImeDrawsImeNavBar && mShouldShowImeSwitcherWhenImeIsShown + && mNavigationBarFrame != null && !isShown(); + if (visible != mCustomImeSwitcherVisible) { + mCustomImeSwitcherVisible = visible; + mService.onCustomImeSwitcherButtonRequestedVisible(mCustomImeSwitcherVisible); + } + } + @Override public String toDebugString() { return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar + " mNavigationBarFrame=" + mNavigationBarFrame + " mShouldShowImeSwitcherWhenImeIsShown=" + mShouldShowImeSwitcherWhenImeIsShown + + " mCustomImeSwitcherVisible=" + mCustomImeSwitcherVisible + " mAppearance=0x" + Integer.toHexString(mAppearance) + " mDarkIntensity=" + mDarkIntensity + " mDrawLegacyNavigationBarBackground=" + mDrawLegacyNavigationBarBackground diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index acda0c5664fe..69bd6685bcac 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -55,7 +55,7 @@ import java.util.concurrent.locks.ReentrantLock; * {@link Looper#myQueue() Looper.myQueue()}. */ @RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("MessageQueue_host") +@RavenwoodRedirectionClass("MessageQueue_ravenwood") public final class MessageQueue { private static final String TAG_L = "LegacyMessageQueue"; private static final String TAG_C = "ConcurrentMessageQueue"; diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java index 9db88d17614b..c2a47d767801 100644 --- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java @@ -54,7 +54,7 @@ import java.util.concurrent.locks.ReentrantLock; * {@link Looper#myQueue() Looper.myQueue()}. */ @RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("MessageQueue_host") +@RavenwoodRedirectionClass("MessageQueue_ravenwood") public final class MessageQueue { private static final String TAG = "ConcurrentMessageQueue"; private static final boolean DEBUG = false; diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java index 9f7b0b71ae95..cae82d010132 100644 --- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java +++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java @@ -45,7 +45,7 @@ import java.util.concurrent.atomic.AtomicLong; * {@link Looper#myQueue() Looper.myQueue()}. */ @RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("MessageQueue_host") +@RavenwoodRedirectionClass("MessageQueue_ravenwood") public final class MessageQueue { private static final String TAG = "MessageQueue"; private static final boolean DEBUG = false; diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java index f3eec13cb20f..2401f3d11bcf 100644 --- a/core/java/android/os/LockedMessageQueue/MessageQueue.java +++ b/core/java/android/os/LockedMessageQueue/MessageQueue.java @@ -48,7 +48,7 @@ import java.util.concurrent.atomic.AtomicLong; * {@link Looper#myQueue() Looper.myQueue()}. */ @RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("MessageQueue_host") +@RavenwoodRedirectionClass("MessageQueue_ravenwood") public final class MessageQueue { private static final String TAG = "LockedMessageQueue"; private static final boolean DEBUG = false; diff --git a/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java b/core/java/android/os/MessageQueue_ravenwood.java index 1b63adc4319f..4033707c3253 100644 --- a/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java +++ b/core/java/android/os/MessageQueue_ravenwood.java @@ -16,13 +16,16 @@ package android.os; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; + import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; -public class MessageQueue_host { +@RavenwoodKeepWholeClass +class MessageQueue_ravenwood { private static final AtomicLong sNextId = new AtomicLong(1); - private static final Map<Long, MessageQueue_host> sInstances = new ConcurrentHashMap<>(); + private static final Map<Long, MessageQueue_ravenwood> sInstances = new ConcurrentHashMap<>(); private boolean mDeleted = false; @@ -37,8 +40,8 @@ public class MessageQueue_host { } } - private static MessageQueue_host getInstance(long id) { - MessageQueue_host q = sInstances.get(id); + private static MessageQueue_ravenwood getInstance(long id) { + MessageQueue_ravenwood q = sInstances.get(id); if (q == null) { throw new RuntimeException("MessageQueue doesn't exist with id=" + id); } @@ -48,7 +51,7 @@ public class MessageQueue_host { public static long nativeInit() { final long id = sNextId.getAndIncrement(); - final MessageQueue_host q = new MessageQueue_host(); + final MessageQueue_ravenwood q = new MessageQueue_ravenwood(); sInstances.put(id, q); return id; } diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java index db323dcf9009..435c34f832c6 100644 --- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java @@ -52,7 +52,7 @@ import java.util.concurrent.atomic.AtomicLong; * {@link Looper#myQueue() Looper.myQueue()}. */ @RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("MessageQueue_host") +@RavenwoodRedirectionClass("MessageQueue_ravenwood") public final class MessageQueue { private static final String TAG = "SemiConcurrentMessageQueue"; private static final boolean DEBUG = false; diff --git a/core/java/android/os/WorkSource.java b/core/java/android/os/WorkSource.java index 6d4e28403908..517418a717fb 100644 --- a/core/java/android/os/WorkSource.java +++ b/core/java/android/os/WorkSource.java @@ -1011,13 +1011,7 @@ public class WorkSource implements Parcelable { return mTags.length > 0 ? mTags[0] : null; } - // TODO: The following three trivial getters are purely for testing and will be removed - // once we have higher level logic in place, e.g for serializing this WorkChain to a proto, - // diffing it etc. - - /** @hide */ - @VisibleForTesting public int[] getUids() { int[] uids = new int[mSize]; System.arraycopy(mUids, 0, uids, 0, mSize); @@ -1025,7 +1019,6 @@ public class WorkSource implements Parcelable { } /** @hide */ - @VisibleForTesting public String[] getTags() { String[] tags = new String[mSize]; System.arraycopy(mTags, 0, tags, 0, mSize); @@ -1033,7 +1026,6 @@ public class WorkSource implements Parcelable { } /** @hide */ - @VisibleForTesting public int getSize() { return mSize; } diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 9c83bc2c88ec..d9db28e0b3c3 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -243,6 +243,15 @@ flag { } flag { + name: "update_engine_api" + namespace: "art_mainline" + description: "Update Engine APIs for ART" + is_exported: true + is_fixed_read_only: true + bug: "377557749" +} + +flag { namespace: "system_performance" name: "perfetto_sdk_tracing" description: "Tracing using Perfetto SDK." diff --git a/core/java/android/os/instrumentation/ExecutableMethodFileOffsets.aidl b/core/java/android/os/instrumentation/ExecutableMethodFileOffsets.aidl new file mode 100644 index 000000000000..dbe54891b0f2 --- /dev/null +++ b/core/java/android/os/instrumentation/ExecutableMethodFileOffsets.aidl @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.os.instrumentation; + +/** + * Represents the location of the code for a compiled method within a process' + * memory. + * {@hide} + */ +@JavaDerive(toString=true) +parcelable ExecutableMethodFileOffsets { + /** + * The OS path of the containing file (could be virtual). + */ + @utf8InCpp String containerPath; + /** + * The offset of the containing file within the process' memory. + */ + long containerOffset; + /** + * The offset of the method within the containing file. + */ + long methodOffset; +} diff --git a/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl new file mode 100644 index 000000000000..c45c51d15cc9 --- /dev/null +++ b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.instrumentation; + +import android.os.instrumentation.ExecutableMethodFileOffsets; +import android.os.instrumentation.MethodDescriptor; +import android.os.instrumentation.TargetProcess; + +/** + * System private API for managing the dynamic attachment of instrumentation. + * + * {@hide} + */ +interface IDynamicInstrumentationManager { + /** Provides ART metadata about the described compiled method within the target process */ + @PermissionManuallyEnforced + @nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets( + in TargetProcess targetProcess, in MethodDescriptor methodDescriptor); +} diff --git a/core/java/android/os/instrumentation/MethodDescriptor.aidl b/core/java/android/os/instrumentation/MethodDescriptor.aidl new file mode 100644 index 000000000000..055d0ecb66e4 --- /dev/null +++ b/core/java/android/os/instrumentation/MethodDescriptor.aidl @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.instrumentation; + +/** + * Represents a JVM method, where class fields that make up its signature. + * {@hide} + */ +@JavaDerive(toString=true) +parcelable MethodDescriptor { + /** + * Fully qualified class in reverse.domain.Naming + */ + @utf8InCpp String fullyQualifiedClassName; + /** + * Name of the method. + */ + @utf8InCpp String methodName; + /** + * Fully qualified types of method parameters, or string representations if primitive e.g. "int". + */ + @utf8InCpp String[] fullyQualifiedParameters; +} diff --git a/core/java/android/os/instrumentation/TargetProcess.aidl b/core/java/android/os/instrumentation/TargetProcess.aidl new file mode 100644 index 000000000000..e90780d07ef2 --- /dev/null +++ b/core/java/android/os/instrumentation/TargetProcess.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.instrumentation; + +/** + * Addresses a process that would run on the device. + * Helps disambiguate targeted processes in cases of pid re-use. + * {@hide} + */ +@JavaDerive(toString=true) +parcelable TargetProcess { + int uid; + int pid; + @utf8InCpp String processName; +} diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 9e0d0e195a96..92c5c20a1f82 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -354,9 +354,10 @@ flag { flag { name: "health_connect_backup_restore_permission_enabled" is_fixed_read_only: true - namespace: "health_connect" + namespace: "health_fitness_aconfig" description: "This flag protects the permission that is required to call Health Connect backup and restore apis" bug: "376014879" # android_fr bug + is_exported: true } flag { @@ -385,3 +386,17 @@ flag { description: "This fixed read-only flag is used to enable new ranging permission for all ranging use cases." bug: "370977414" } + +flag { + name: "system_selection_toolbar_enabled" + namespace: "permissions" + description: "Enables the system selection toolbar feature." + bug: "363318732" +} + +flag { + name: "use_system_selection_toolbar_in_sysui" + namespace: "permissions" + description: "Uses the SysUi process to host the SelectionToolbarRenderService." + bug: "363318732" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d19681c86320..ef351719ea70 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8687,6 +8687,19 @@ public final class Settings { public static final String ACCESSIBILITY_QS_TARGETS = "accessibility_qs_targets"; /** + * Setting specifying the accessibility services, accessibility shortcut targets, + * or features to be toggled via a keyboard shortcut gesture. + * + * <p> This is a colon-separated string list which contains the flattened + * {@link ComponentName} and the class name of a system class implementing a supported + * accessibility feature. + * + * @hide + */ + public static final String ACCESSIBILITY_KEY_GESTURE_TARGETS = + "accessibility_key_gesture_targets"; + + /** * The system class name of magnification controller which is a target to be toggled via * accessibility shortcut or accessibility button. * diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 1d35344e5218..7cb0ffcfcc72 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -120,3 +120,10 @@ flag { description: "Feature flag for exposing KeyStore grant APIs" bug: "351158708" } + +flag { + name: "secure_lockdown" + namespace: "biometrics" + description: "Feature flag for Secure Lockdown feature" + bug: "373422357" +}
\ No newline at end of file diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig index 72f2de805474..dfc11dcb5427 100644 --- a/core/java/android/service/dreams/flags.aconfig +++ b/core/java/android/service/dreams/flags.aconfig @@ -67,3 +67,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "cleanup_dream_settings_on_uninstall" + namespace: "systemui" + description: "Cleans up dream settings if dream package is uninstalled." + bug: "338210427" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl new file mode 100644 index 000000000000..64a8b90fe581 --- /dev/null +++ b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl @@ -0,0 +1,18 @@ +package android.service.settings.preferences; + +import android.service.settings.preferences.GetValueRequest; +import android.service.settings.preferences.IGetValueCallback; +import android.service.settings.preferences.IMetadataCallback; +import android.service.settings.preferences.ISetValueCallback; +import android.service.settings.preferences.MetadataRequest; +import android.service.settings.preferences.SetValueRequest; + +/** @hide */ +oneway interface ISettingsPreferenceService { + @EnforcePermission("READ_SYSTEM_PREFERENCES") + void getAllPreferenceMetadata(in MetadataRequest request, IMetadataCallback callback) = 1; + @EnforcePermission("READ_SYSTEM_PREFERENCES") + void getPreferenceValue(in GetValueRequest request, IGetValueCallback callback) = 2; + @EnforcePermission(allOf = {"READ_SYSTEM_PREFERENCES", "WRITE_SYSTEM_PREFERENCES"}) + void setPreferenceValue(in SetValueRequest request, ISetValueCallback callback) = 3; +} diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceService.java b/core/java/android/service/settings/preferences/SettingsPreferenceService.java new file mode 100644 index 000000000000..4a4b5d201f09 --- /dev/null +++ b/core/java/android/service/settings/preferences/SettingsPreferenceService.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.preferences; + +import android.Manifest; +import android.annotation.EnforcePermission; +import android.annotation.FlaggedApi; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.IBinder; +import android.os.OutcomeReceiver; +import android.os.PermissionEnforcer; +import android.os.RemoteException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.flags.Flags; + +/** + * Base class for a service that exposes its settings preferences to external access. + * <p>This class is to be implemented by apps that contribute to the Android Settings surface. + * Access to this service is permission guarded by + * {@link android.permission.READ_SYSTEM_PREFERENCES} for binding and reading, and guarded by both + * {@link android.permission.READ_SYSTEM_PREFERENCES} and + * {@link android.permission.WRITE_SYSTEM_PREFERENCES} for writing. An additional checks for access + * control are the responsibility of the implementing class. + * + * <p>This implementation must correspond to an exported service declaration in the host app + * AndroidManifest.xml as follows + * <pre class="prettyprint"> + * {@literal + * <service + * android:permission="android.permission.READ_SYSTEM_PREFERENCES" + * android:exported="true"> + * <intent-filter> + * <action android:name="android.service.settings.preferences.action.PREFERENCE_SERVICE" /> + * </intent-filter> + * </service>} + * </pre> + * + * <ul> + * <li>It is recommended to expose the metadata for most, if not all, preferences within a + * settings app, thus implementing {@link #onGetAllPreferenceMetadata}. + * <li>Exposing preferences for read access of their values is up to the implementer, but any + * exposed must be a subset of the preferences exposed in {@link #onGetAllPreferenceMetadata}. + * To expose a preference for read access, the implementation will contain + * {@link #onGetPreferenceValue}. + * <li>Exposing a preference for write access of their values is up to the implementer, but should + * be done so with extra care and consideration, both for security and privacy. These must also + * be a subset of those exposed in {@link #onGetAllPreferenceMetadata}. To expose a preference for + * write access, the implementation will contain {@link #onSetPreferenceValue}. + * </ul> + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public abstract class SettingsPreferenceService extends Service { + + /** + * Intent Action corresponding to a {@link SettingsPreferenceService}. Note that any checks for + * such services must be accompanied by a check to ensure the host is a system application. + * Given an {@link android.content.pm.ApplicationInfo} you can check for + * {@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}, or when querying + * {@link PackageManager#queryIntentServices} you can provide the flag + * {@link PackageManager#MATCH_SYSTEM_ONLY}. + */ + public static final String ACTION_PREFERENCE_SERVICE = + "android.service.settings.preferences.action.PREFERENCE_SERVICE"; + + /** @hide */ + @NonNull + @Override + public final IBinder onBind(@Nullable Intent intent) { + return new ISettingsPreferenceService.Stub( + PermissionEnforcer.fromContext(getApplicationContext())) { + @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES) + @Override + public void getAllPreferenceMetadata(MetadataRequest request, + IMetadataCallback callback) { + getAllPreferenceMetadata_enforcePermission(); + onGetAllPreferenceMetadata(request, new OutcomeReceiver<>() { + @Override + public void onResult(MetadataResult result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void onError(@NonNull Exception error) { + try { + callback.onFailure(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + }); + } + + @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES) + @Override + public void getPreferenceValue(GetValueRequest request, IGetValueCallback callback) { + getPreferenceValue_enforcePermission(); + onGetPreferenceValue(request, new OutcomeReceiver<>() { + @Override + public void onResult(GetValueResult result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void onError(@NonNull Exception error) { + try { + callback.onFailure(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + }); + } + + @EnforcePermission(allOf = { + Manifest.permission.READ_SYSTEM_PREFERENCES, + Manifest.permission.WRITE_SYSTEM_PREFERENCES + }) + @Override + public void setPreferenceValue(SetValueRequest request, ISetValueCallback callback) { + setPreferenceValue_enforcePermission(); + onSetPreferenceValue(request, new OutcomeReceiver<>() { + @Override + public void onResult(SetValueResult result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void onError(@NonNull Exception error) { + try { + callback.onFailure(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + }); + } + }; + } + + /** + * Retrieve the metadata for all exposed settings preferences within this application. This + * data should be a snapshot of their state at the time of this method being called. + * @param request object to specify request parameters + * @param callback object to receive result or failure of request + */ + public abstract void onGetAllPreferenceMetadata( + @NonNull MetadataRequest request, + @NonNull OutcomeReceiver<MetadataResult, Exception> callback); + + /** + * Retrieve the current value of the requested settings preference. If this value is not exposed + * or cannot be obtained for some reason, the corresponding result code will be set on the + * result object. + * @param request object to specify request parameters + * @param callback object to receive result or failure of request + */ + public abstract void onGetPreferenceValue( + @NonNull GetValueRequest request, + @NonNull OutcomeReceiver<GetValueResult, Exception> callback); + + /** + * Set the value within the request to the target settings preference. If this value cannot + * be written for some reason, the corresponding result code will be set on the result object. + * @param request object to specify request parameters + * @param callback object to receive result or failure of request + */ + public abstract void onSetPreferenceValue( + @NonNull SetValueRequest request, + @NonNull OutcomeReceiver<SetValueResult, Exception> callback); +} diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java new file mode 100644 index 000000000000..39995a47fcbe --- /dev/null +++ b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.preferences; + +import static android.service.settings.preferences.SettingsPreferenceService.ACTION_PREFERENCE_SERVICE; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.TestApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.IBinder; +import android.os.OutcomeReceiver; +import android.os.RemoteException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.flags.Flags; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Client class responsible for binding to and interacting with an instance of + * {@link SettingsPreferenceService}. + * <p>This is a convenience class to handle the lifecycle of the service connection. + * <p>This client will only interact with one instance at a time, + * so if the caller requires multiple instances (multiple applications that provide settings), then + * the caller must create multiple client classes, one for each instance required. To find all + * available services, a caller may query {@link android.content.pm.PackageManager} for applications + * that provide the intent action {@link SettingsPreferenceService#ACTION_PREFERENCE_SERVICE} that + * are also system applications ({@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}). + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public class SettingsPreferenceServiceClient implements AutoCloseable { + + private final Context mContext; + private final Intent mServiceIntent; + private final ServiceConnection mServiceConnection; + private final boolean mSystemOnly; + private ISettingsPreferenceService mRemoteService; + + /** + * Construct a client for binding to a {@link SettingsPreferenceService} provided by the + * application corresponding to the provided package name. + * @param packageName - package name for which this client will initiate a service binding + */ + public SettingsPreferenceServiceClient(@NonNull Context context, + @NonNull String packageName) { + this(context, packageName, true, null); + } + + /** + * @hide Only to be called directly by test + */ + @TestApi + public SettingsPreferenceServiceClient(@NonNull Context context, + @NonNull String packageName, + boolean systemOnly, + @Nullable ServiceConnection connectionListener) { + mContext = context.getApplicationContext(); + mServiceIntent = new Intent(ACTION_PREFERENCE_SERVICE).setPackage(packageName); + mSystemOnly = systemOnly; + mServiceConnection = createServiceConnection(connectionListener); + } + + /** + * Initiate binding to service. + * <p>If no service exists for the package provided or the package is not for a system + * application, no binding will occur. + */ + public void start() { + PackageManager pm = mContext.getPackageManager(); + PackageManager.ResolveInfoFlags flags; + if (mSystemOnly) { + flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY); + } else { + flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL); + } + List<ResolveInfo> infos = pm.queryIntentServices(mServiceIntent, flags); + if (infos.size() == 1) { + mContext.bindService(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE); + } + } + + /** + * If there is an active service binding, unbind from that service. + */ + public void stop() { + if (mRemoteService != null) { + mRemoteService = null; + mContext.unbindService(mServiceConnection); + } + } + + /** + * Retrieve the metadata for all exposed settings preferences within the application. + * @param request object to specify request parameters + * @param executor {@link Executor} on which to invoke the receiver + * @param receiver callback to receive the result or failure + */ + public void getAllPreferenceMetadata( + @NonNull MetadataRequest request, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<MetadataResult, Exception> receiver) { + if (mRemoteService == null) { + executor.execute(() -> + receiver.onError(new IllegalStateException("Service not ready"))); + return; + } + try { + mRemoteService.getAllPreferenceMetadata(request, new IMetadataCallback.Stub() { + @Override + public void onSuccess(MetadataResult result) { + executor.execute(() -> receiver.onResult(result)); + } + + @Override + public void onFailure() { + executor.execute(() -> receiver.onError( + new IllegalStateException("Service call failure"))); + } + }); + } catch (RemoteException | RuntimeException e) { + executor.execute(() -> receiver.onError(e)); + } + } + + /** + * Retrieve the current value of the requested settings preference. + * @param request object to specify request parameters + * @param executor {@link Executor} on which to invoke the receiver + * @param receiver callback to receive the result or failure + */ + public void getPreferenceValue(@NonNull GetValueRequest request, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<GetValueResult, Exception> receiver) { + if (mRemoteService == null) { + executor.execute(() -> + receiver.onError(new IllegalStateException("Service not ready"))); + return; + } + try { + mRemoteService.getPreferenceValue(request, new IGetValueCallback.Stub() { + @Override + public void onSuccess(GetValueResult result) { + executor.execute(() -> receiver.onResult(result)); + } + + @Override + public void onFailure() { + executor.execute(() -> receiver.onError( + new IllegalStateException("Service call failure"))); + } + }); + } catch (RemoteException | RuntimeException e) { + executor.execute(() -> receiver.onError(e)); + } + } + + /** + * Set the value on the target settings preference. + * @param request object to specify request parameters + * @param executor {@link Executor} on which to invoke the receiver + * @param receiver callback to receive the result or failure + */ + public void setPreferenceValue(@NonNull SetValueRequest request, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<SetValueResult, Exception> receiver) { + if (mRemoteService == null) { + executor.execute(() -> + receiver.onError(new IllegalStateException("Service not ready"))); + return; + } + try { + mRemoteService.setPreferenceValue(request, new ISetValueCallback.Stub() { + @Override + public void onSuccess(SetValueResult result) { + executor.execute(() -> receiver.onResult(result)); + } + + @Override + public void onFailure() { + executor.execute(() -> receiver.onError( + new IllegalStateException("Service call failure"))); + } + }); + } catch (RemoteException | RuntimeException e) { + executor.execute(() -> receiver.onError(e)); + } + } + + @NonNull + private ServiceConnection createServiceConnection(@Nullable ServiceConnection listener) { + return new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mRemoteService = getPreferenceServiceInterface(service); + if (listener != null) { + listener.onServiceConnected(name, service); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mRemoteService = null; + if (listener != null) { + listener.onServiceDisconnected(name); + } + } + }; + } + + @NonNull + private ISettingsPreferenceService getPreferenceServiceInterface(@NonNull IBinder service) { + return ISettingsPreferenceService.Stub.asInterface(service); + } + + /** + * This client handles a resource, thus is it important to appropriately close that resource + * when it is no longer needed. + * <p>This method is provided by {@link AutoCloseable} and calling it + * will unbind any service binding. + */ + @Override + public void close() { + stop(); + } +} diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index bce51f297aff..1df3b4332754 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -37,6 +37,7 @@ import android.telephony.TelephonyManager.EmergencyCallbackModeType; import android.telephony.emergency.EmergencyNumber; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.MediaQualityStatus; +import android.telephony.satellite.NtnSignalStrength; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.IPhoneStateListener; @@ -1706,6 +1707,11 @@ public class PhoneStateListener { @NetworkRegistrationInfo.ServiceType int[] availableServices) { // not supported on the deprecated interface - Use TelephonyCallback instead } + + public final void onCarrierRoamingNtnSignalStrengthChanged( + @NonNull NtnSignalStrength ntnSignalStrength) { + // not supported on the deprecated interface - Use TelephonyCallback instead + } } private void log(String s) { diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java index 64a5533cbe69..0d1dc4611343 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -30,6 +30,7 @@ import android.telephony.emergency.EmergencyNumber; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.MediaQualityStatus; import android.telephony.ims.MediaThreshold; +import android.telephony.satellite.NtnSignalStrength; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -695,6 +696,15 @@ public class TelephonyCallback { public static final int EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED = 44; /** + * Event for listening to carrier roaming non-terrestrial network signal strength changes. + * + * @see CarrierRoamingNtnModeListener + * + * @hide + */ + public static final int EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED = 45; + + /** * @hide */ @IntDef(prefix = {"EVENT_"}, value = { @@ -741,7 +751,8 @@ public class TelephonyCallback { EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED, EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED, EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED, - EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED + EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED, + EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface TelephonyEvent { @@ -1805,6 +1816,14 @@ public class TelephonyCallback { */ default void onCarrierRoamingNtnAvailableServicesChanged( @NetworkRegistrationInfo.ServiceType List<Integer> availableServices) {} + + /** + * Callback invoked when carrier roaming non-terrestrial network signal strength changes. + * + * @param ntnSignalStrength non-terrestrial network signal strength. + */ + default void onCarrierRoamingNtnSignalStrengthChanged( + @NonNull NtnSignalStrength ntnSignalStrength) {} } /** @@ -2270,5 +2289,18 @@ public class TelephonyCallback { Binder.withCleanCallingIdentity(() -> mExecutor.execute( () -> listener.onCarrierRoamingNtnAvailableServicesChanged(ServiceList))); } + + public void onCarrierRoamingNtnSignalStrengthChanged( + @NonNull NtnSignalStrength ntnSignalStrength) { + if (!Flags.carrierRoamingNbIotNtn()) return; + + CarrierRoamingNtnModeListener listener = + (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get(); + if (listener == null) return; + + Binder.withCleanCallingIdentity(() -> mExecutor.execute( + () -> listener.onCarrierRoamingNtnSignalStrengthChanged(ntnSignalStrength))); + + } } } diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 1dab2cf75594..90b0bb34c145 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -47,6 +47,7 @@ import android.telephony.emergency.EmergencyNumber; import android.telephony.ims.ImsCallSession; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.MediaQualityStatus; +import android.telephony.satellite.NtnSignalStrength; import android.telephony.satellite.SatelliteStateChangeListener; import android.util.ArrayMap; import android.util.ArraySet; @@ -1137,6 +1138,23 @@ public class TelephonyRegistryManager { } /** + * Notify external listeners that carrier roaming non-terrestrial network + * signal strength changed. + * @param subId subscription ID. + * @param ntnSignalStrength non-terrestrial network signal strength. + * @hide + */ + public final void notifyCarrierRoamingNtnSignalStrengthChanged(int subId, + @NonNull NtnSignalStrength ntnSignalStrength) { + try { + sRegistry.notifyCarrierRoamingNtnSignalStrengthChanged(subId, ntnSignalStrength); + } catch (RemoteException ex) { + // system server crash + throw ex.rethrowFromSystemServer(); + } + } + + /** * Processes potential event changes from the provided {@link TelephonyCallback}. * * @param telephonyCallback callback for monitoring callback changes to the telephony state. @@ -1293,6 +1311,7 @@ public class TelephonyRegistryManager { eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED); eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED); eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED); + eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED); } return eventList; } diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 02923eda308e..f43f172d7d5b 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -163,10 +163,12 @@ flag { } flag { - name: "typeface_redesign" + name: "typeface_redesign_readonly" namespace: "text" description: "Decouple variation settings, weight and style information from Typeface class" bug: "361260253" + # This feature does not support runtime flag switch which leads crash in System UI. + is_fixed_read_only: true } flag { diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index dddc408ed9db..38e4e2760d25 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -935,7 +935,6 @@ public class KeyEvent extends InputEvent implements Parcelable { */ public static final int KEYCODE_MACRO_4 = 316; /** Key code constant: To open emoji picker */ - @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE) public static final int KEYCODE_EMOJI_PICKER = 317; /** * Key code constant: To take a screenshot @@ -944,15 +943,80 @@ public class KeyEvent extends InputEvent implements Parcelable { * unlike {@code KEYCODE_SYSRQ} which is sent to the app first and only if the app * doesn't handle it, the framework handles it (to take a screenshot). */ - @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE) public static final int KEYCODE_SCREENSHOT = 318; + /** Key code constant: To start dictate to an input field */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_DICTATE = 319; + /** + * Key code constant: AC New + * + * e.g. To create a new instance of a window, open a new tab, etc. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_NEW = 320; + /** + * Key code constant: AC Close + * + * e.g. To close current instance of the application window, close the current tab, etc. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_CLOSE = 321; + /** Key code constant: To toggle 'Do Not Disturb' mode */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_DO_NOT_DISTURB = 322; + /** Key code constant: To Print */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_PRINT = 323; + /** Key code constant: To Lock the screen */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_LOCK = 324; + /** Key code constant: To toggle fullscreen mode (on the current application) */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_FULLSCREEN = 325; + /** Key code constant: F13 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F13 = 326; + /** Key code constant: F14 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F14 = 327; + /** Key code constant: F15 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F15 = 328; + /** Key code constant: F16 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F16 = 329; + /** Key code constant: F17 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F17 = 330; + /** Key code constant: F18 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F18 = 331; + /** Key code constant: F19 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F19 = 332; + /** Key code constant: F20 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F20 = 333; + /** Key code constant: F21 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F21 = 334; + /** Key code constant: F22 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F22 = 335; + /** Key code constant: F23 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F23 = 336; + /** Key code constant: F24 key. */ + @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + public static final int KEYCODE_F24 = 337; /** * Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent. * @hide */ @TestApi - public static final int LAST_KEYCODE = KEYCODE_SCREENSHOT; + @SuppressWarnings("FlaggedApi") + public static final int LAST_KEYCODE = KEYCODE_F24; /** @hide */ @IntDef(prefix = {"KEYCODE_"}, value = { @@ -1275,6 +1339,25 @@ public class KeyEvent extends InputEvent implements Parcelable { KEYCODE_MACRO_4, KEYCODE_EMOJI_PICKER, KEYCODE_SCREENSHOT, + KEYCODE_DICTATE, + KEYCODE_NEW, + KEYCODE_CLOSE, + KEYCODE_DO_NOT_DISTURB, + KEYCODE_PRINT, + KEYCODE_LOCK, + KEYCODE_FULLSCREEN, + KEYCODE_F13, + KEYCODE_F14, + KEYCODE_F15, + KEYCODE_F16, + KEYCODE_F17, + KEYCODE_F18, + KEYCODE_F19, + KEYCODE_F20, + KEYCODE_F21, + KEYCODE_F22, + KEYCODE_F23, + KEYCODE_F24, }) @Retention(RetentionPolicy.SOURCE) @interface KeyCode {} diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index df54d310059f..206c73756088 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -2989,7 +2989,6 @@ public final class SurfaceControl implements Parcelable { private void apply(boolean sync, boolean oneWay) { applyResizedSurfaces(); notifyReparentedSurfaces(); - nativeApplyTransaction(mNativeObject, sync, oneWay); if (SurfaceControlRegistry.sCallStackDebuggingEnabled) { SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging( @@ -2998,6 +2997,7 @@ public final class SurfaceControl implements Parcelable { if (mCalls != null) { mCalls.clear(); } + nativeApplyTransaction(mNativeObject, sync, oneWay); } /** diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 618843ccb005..fa06831f6514 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -23882,12 +23882,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } else { draw(canvas); } - } - // For VRR to vote the preferred frame rate - if (sToolkitSetFrameRateReadOnlyFlagValue - && sToolkitFrameRateViewEnablingReadOnlyFlagValue) { - votePreferredFrameRate(); + // For VRR to vote the preferred frame rate + if (sToolkitSetFrameRateReadOnlyFlagValue + && sToolkitFrameRateViewEnablingReadOnlyFlagValue) { + votePreferredFrameRate(); + } } } finally { renderNode.endRecording(); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9a2aa0b8a682..75d2da1b70e4 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -9951,11 +9951,13 @@ public final class ViewRootImpl implements ViewParent, return false; } - if (!mIsDrawing) { - destroyHardwareRenderer(); - } else { - Log.e(mTag, "Attempting to destroy the window while drawing!\n" + - " window=" + this + ", title=" + mWindowAttributes.getTitle()); + if (!com.android.graphics.hwui.flags.Flags.removeVriSketchyDestroy()) { + if (!mIsDrawing) { + destroyHardwareRenderer(); + } else { + Log.e(mTag, "Attempting to destroy the window while drawing!\n" + + " window=" + this + ", title=" + mWindowAttributes.getTitle()); + } } mHandler.sendEmptyMessage(MSG_DIE); return true; @@ -9976,9 +9978,9 @@ public final class ViewRootImpl implements ViewParent, dispatchDetachedFromWindow(); } - if (mAdded && !mFirst) { - destroyHardwareRenderer(); + destroyHardwareRenderer(); + if (mAdded && !mFirst) { if (mView != null) { int viewVisibility = mView.getVisibility(); boolean viewVisibilityChanged = mViewVisibility != viewVisibility; diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 14652035438d..0204517e869a 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -3866,8 +3866,14 @@ public class AccessibilityNodeInfo implements Parcelable { * Sets the view for which the view represented by this info serves as a * label for accessibility purposes. * + * @deprecated Use {@link #addLabeledBy(View)} on the labeled node instead, + * since {@link #getLabeledByList()} and {@link #getLabeledBy()} on the + * labeled node are not automatically populated when this method is used. + * * @param labeled The view for which this info serves as a label. */ + @FlaggedApi(Flags.FLAG_DEPRECATE_ANI_LABEL_FOR_APIS) + @Deprecated public void setLabelFor(View labeled) { setLabelFor(labeled, AccessibilityNodeProvider.HOST_VIEW_ID); } @@ -3888,9 +3894,15 @@ public class AccessibilityNodeInfo implements Parcelable { * This class is made immutable before being delivered to an AccessibilityService. * </p> * + * @deprecated Use {@link #addLabeledBy(View)} on the labeled node instead, + * since {@link #getLabeledByList()} and {@link #getLabeledBy()} on the + * labeled node are not automatically populated when this method is used. + * * @param root The root whose virtual descendant serves as a label. * @param virtualDescendantId The id of the virtual descendant. */ + @FlaggedApi(Flags.FLAG_DEPRECATE_ANI_LABEL_FOR_APIS) + @Deprecated public void setLabelFor(View root, int virtualDescendantId) { enforceNotSealed(); final int rootAccessibilityViewId = (root != null) @@ -3902,8 +3914,14 @@ public class AccessibilityNodeInfo implements Parcelable { * Gets the node info for which the view represented by this info serves as * a label for accessibility purposes. * + * @deprecated Use {@link #getLabeledByList()} on the labeled node instead, + * since calling {@link #addLabeledBy(View)} or {@link #addLabeledBy(View, int)} + * on the labeled node do not automatically provide that node from this method. + * * @return The labeled info. */ + @FlaggedApi(Flags.FLAG_DEPRECATE_ANI_LABEL_FOR_APIS) + @Deprecated public AccessibilityNodeInfo getLabelFor() { enforceSealed(); return getNodeForAccessibilityId(mConnectionId, mWindowId, mLabelForId); diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 7177ef330f06..8a006fa5b509 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -92,6 +92,13 @@ flag { flag { namespace: "accessibility" + name: "deprecate_ani_label_for_apis" + description: "Controls the deprecation of AccessibilityNodeInfo labelFor apis" + bug: "333783827" +} + +flag { + namespace: "accessibility" name: "fix_merged_content_change_event_v2" description: "Fixes event type and source of content change event merged in ViewRootImpl" bug: "277305460" diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index 0ab51e45a951..905f350ca6c5 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -316,6 +316,35 @@ public class AutofillFeatureFlags { // END AUTOFILL PCC CLASSIFICATION FLAGS + // START AUTOFILL REMOVE PRE_TRIGGER FLAGS + + /** + * Whether pre-trigger flow is disabled. + * + * @hide + */ + public static final String DEVICE_CONFIG_IMPROVE_FILL_DIALOG_ENABLED = "improve_fill_dialog"; + + /** + * Minimum amount of time (in milliseconds) to wait after IME animation finishes, and before + * starting fill dialog animation. + * + * @hide + */ + public static final String DEVICE_CONFIG_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS = + "fill_dialog_min_wait_after_animation_end_ms"; + + /** + * Sets a value of timeout in milliseconds, measured after animation end, during which fill + * dialog can be shown. If we are at time > animation_end_time + this timeout, fill dialog + * wouldn't be shown. + * + * @hide + */ + public static final String DEVICE_CONFIG_FILL_DIALOG_TIMEOUT_MS = "fill_dialog_timeout_ms"; + + // END AUTOFILL REMOVE PRE_TRIGGER FLAGS + /** * Define the max input length for autofill to show suggesiton UI * @@ -366,6 +395,17 @@ public class AutofillFeatureFlags { DEFAULT_AFAA_SHOULD_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE = true; // END AUTOFILL FOR ALL APPS DEFAULTS + // START AUTOFILL REMOVE PRE_TRIGGER FLAGS DEFAULTS + // Default for whether the pre trigger removal is enabled. + /** @hide */ + public static final boolean DEFAULT_IMPROVE_FILL_DIALOG_ENABLED = true; + // Default for whether the pre trigger removal is enabled. + /** @hide */ + public static final long DEFAULT_FILL_DIALOG_TIMEOUT_MS = 300; // 300 ms + /** @hide */ + public static final long DEFAULT_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS = 0; // 0 ms + // END AUTOFILL REMOVE PRE_TRIGGER FLAGS DEFAULTS + /** * @hide */ @@ -611,4 +651,48 @@ public class AutofillFeatureFlags { } // END AUTOFILL PCC CLASSIFICATION FUNCTIONS + + + // START AUTOFILL REMOVE PRE_TRIGGER + /** + * Whether Autofill Pre Trigger Removal is enabled. + * + * @hide + */ + public static boolean isImproveFillDialogEnabled() { + // TODO(b/266379948): Add condition for checking whether device has PCC first + + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_IMPROVE_FILL_DIALOG_ENABLED, + DEFAULT_IMPROVE_FILL_DIALOG_ENABLED); + } + + /** + * Whether Autofill Pre Trigger Removal is enabled. + * + * @hide + */ + public static long getFillDialogTimeoutMs() { + // TODO(b/266379948): Add condition for checking whether device has PCC first + + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_FILL_DIALOG_TIMEOUT_MS, + DEFAULT_FILL_DIALOG_TIMEOUT_MS); + } + + /** + * Whether Autofill Pre Trigger Removal is enabled. + * + * @hide + */ + public static long getFillDialogMinWaitAfterImeAnimationtEndMs() { + // TODO(b/266379948): Add condition for checking whether device has PCC first + + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS, + DEFAULT_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS); + } } diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index aa4927ee9b9c..edd9d6cff799 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -158,3 +158,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "writing_tools" + namespace: "input_method" + description: "Writing tools API" + bug: "373788889" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index dae87ddcb1bd..6b5a367ab460 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -75,7 +75,8 @@ public enum DesktopModeFlags { Flags::enableDesktopAppLaunchAlttabTransitions, false), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS( Flags::enableDesktopAppLaunchTransitions, false), - ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false); + ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false), + ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true); private static final String TAG = "DesktopModeFlagsUtil"; // Function called to obtain aconfig flag value. diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index d39ecabbb2d2..f474b34ac390 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -353,6 +353,16 @@ flag { } flag { + name: "enable_desktop_system_dialogs_transitions" + namespace: "lse_desktop_experience" + description: "Enables custom transitions for system dialogs in Desktop Mode." + bug: "335638193" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_move_to_next_display_shortcut" namespace: "lse_desktop_experience" description: "Add new keyboard shortcut of moving a task into next display" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 68e78fed29c5..d9de38a8bd34 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -268,6 +268,16 @@ flag { } flag { + name: "system_ui_post_animation_end" + namespace: "windowing_frontend" + description: "Run AnimatorListener#onAnimationEnd on next frame for SystemUI" + bug: "300035126" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "system_ui_immersive_confirmation_dialog" namespace: "windowing_frontend" description: "Enable the implementation of the immersive confirmation dialog on system UI side by default" diff --git a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java index 44dceb9b7edb..4a49bb6720ef 100644 --- a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java +++ b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java @@ -63,6 +63,8 @@ public final class ShortcutConstants { * quickly tapping screen 2 times with two fingers as preferred shortcut. * {@code QUICK_SETTINGS} for displaying specifying the accessibility services or features which * choose Quick Settings as preferred shortcut. + * {@code KEY_GESTURE} for shortcuts which are directly from key gestures and should be + * activated always. */ @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -73,6 +75,7 @@ public final class ShortcutConstants { UserShortcutType.TWOFINGER_DOUBLETAP, UserShortcutType.QUICK_SETTINGS, UserShortcutType.GESTURE, + UserShortcutType.KEY_GESTURE, UserShortcutType.ALL }) public @interface UserShortcutType { @@ -84,8 +87,10 @@ public final class ShortcutConstants { int TWOFINGER_DOUBLETAP = 1 << 3; int QUICK_SETTINGS = 1 << 4; int GESTURE = 1 << 5; + int KEY_GESTURE = 1 << 6; // LINT.ThenChange(:shortcut_type_array) - int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE; + int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE + | KEY_GESTURE; } /** @@ -99,7 +104,8 @@ public final class ShortcutConstants { UserShortcutType.TRIPLETAP, UserShortcutType.TWOFINGER_DOUBLETAP, UserShortcutType.QUICK_SETTINGS, - UserShortcutType.GESTURE + UserShortcutType.GESTURE, + UserShortcutType.KEY_GESTURE // LINT.ThenChange(:shortcut_type_intdef) }; diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java index 2e0ff3db6c50..14ca0f8cae69 100644 --- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java +++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java @@ -27,6 +27,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.SERVIC import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; @@ -187,6 +188,7 @@ public final class ShortcutUtils { case TWOFINGER_DOUBLETAP -> Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED; case QUICK_SETTINGS -> Settings.Secure.ACCESSIBILITY_QS_TARGETS; + case KEY_GESTURE -> Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS; default -> throw new IllegalArgumentException( "Unsupported user shortcut type: " + type); }; @@ -209,6 +211,7 @@ public final class ShortcutUtils { TRIPLETAP; case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED -> TWOFINGER_DOUBLETAP; + case Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS -> KEY_GESTURE; default -> throw new IllegalArgumentException( "Unsupported user shortcut key: " + key); }; diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java index b3480ab92f46..2931bd2c83dd 100644 --- a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java +++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java @@ -56,7 +56,7 @@ import libcore.util.NativeAllocationRegistry; * @hide */ @RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("LongArrayMultiStateCounter_host") +@RavenwoodRedirectionClass("LongArrayMultiStateCounter_ravenwood") public final class LongArrayMultiStateCounter implements Parcelable { private static volatile NativeAllocationRegistry sRegistry; private final int mStateCount; diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java index 90608f6e87d1..7030d8e84b70 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java +++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java @@ -18,6 +18,7 @@ package com.android.internal.os; import android.os.BadParcelableException; import android.os.Parcel; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import java.util.Arrays; import java.util.HashMap; @@ -25,7 +26,8 @@ import java.util.HashMap; /** * Native implementation substitutions for the LongArrayMultiStateCounter class. */ -public class LongArrayMultiStateCounter_host { +@RavenwoodKeepWholeClass +class LongArrayMultiStateCounter_ravenwood { /** * A reimplementation of {@link LongArrayMultiStateCounter}, only in diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java index c386a86f5906..ee855d58c874 100644 --- a/core/java/com/android/internal/os/LongMultiStateCounter.java +++ b/core/java/com/android/internal/os/LongMultiStateCounter.java @@ -60,7 +60,7 @@ import libcore.util.NativeAllocationRegistry; * @hide */ @RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("LongMultiStateCounter_host") +@RavenwoodRedirectionClass("LongMultiStateCounter_ravenwood") public final class LongMultiStateCounter implements Parcelable { private static NativeAllocationRegistry sRegistry; diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java b/core/java/com/android/internal/os/LongMultiStateCounter_ravenwood.java index 1d95aa143549..42db37f1f82f 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java +++ b/core/java/com/android/internal/os/LongMultiStateCounter_ravenwood.java @@ -18,13 +18,15 @@ package com.android.internal.os; import android.os.BadParcelableException; import android.os.Parcel; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import java.util.HashMap; /** * Native implementation substitutions for the LongMultiStateCounter class. */ -public class LongMultiStateCounter_host { +@RavenwoodKeepWholeClass +class LongMultiStateCounter_ravenwood { /** * A reimplementation of {@link com.android.internal.os.LongMultiStateCounter}, only in diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl index b5c87868af12..0e85e046e1b6 100644 --- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl +++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl @@ -27,6 +27,7 @@ import android.telephony.PhoneCapability; import android.telephony.PhysicalChannelConfig; import android.telephony.PreciseCallState; import android.telephony.PreciseDataConnectionState; +import android.telephony.satellite.NtnSignalStrength; import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.emergency.EmergencyNumber; @@ -85,4 +86,5 @@ oneway interface IPhoneStateListener { void onCarrierRoamingNtnModeChanged(in boolean active); void onCarrierRoamingNtnEligibleStateChanged(in boolean eligible); void onCarrierRoamingNtnAvailableServicesChanged(in int[] availableServices); + void onCarrierRoamingNtnSignalStrengthChanged(in NtnSignalStrength ntnSignalStrength); } diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl index 1c76a6cd4bba..0f268d5de62b 100644 --- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -29,6 +29,7 @@ import android.telephony.ims.ImsReasonInfo; import android.telephony.PhoneCapability; import android.telephony.PhysicalChannelConfig; import android.telephony.PreciseDataConnectionState; +import android.telephony.satellite.NtnSignalStrength; import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.emergency.EmergencyNumber; @@ -125,8 +126,10 @@ interface ITelephonyRegistry { void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active); void notifyCarrierRoamingNtnEligibleStateChanged(int subId, in boolean eligible); void notifyCarrierRoamingNtnAvailableServicesChanged(int subId, in int[] availableServices); + void notifyCarrierRoamingNtnSignalStrengthChanged(int subId, in NtnSignalStrength ntnSignalStrength); void addSatelliteStateChangeListener(ISatelliteStateChangeListener listener, String pkg, String featureId); void removeSatelliteStateChangeListener(ISatelliteStateChangeListener listener, String pkg); void notifySatelliteStateChanged(boolean isEnabled); + } diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java index 30deb499594c..fb6937c94a3e 100644 --- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java +++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java @@ -63,6 +63,7 @@ public final class NotificationProgressDrawable extends Drawable { private final ArrayList<Part> mParts = new ArrayList<>(); + private final RectF mSegRectF = new RectF(); private final Rect mPointRect = new Rect(); private final RectF mPointRectF = new RectF(); @@ -198,22 +199,42 @@ public final class NotificationProgressDrawable extends Drawable { mState.mSegSegGap, x + segWidth, totalWidth); final float end = x + segWidth - endOffset; - // Transparent is not allowed (and also is the default in the data), so use that - // as a sentinel to be replaced by default - mStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor - : mState.mStrokeColor); - mDashedStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor - : mState.mFadedStrokeColor); - - // Leave space for the rounded line cap which extends beyond start/end. - final float capWidth = mStrokePaint.getStrokeWidth() / 2F; - - canvas.drawLine(start + capWidth, centerY, end - capWidth, centerY, - segment.mDashed ? mDashedStrokePaint : mStrokePaint); - // Advance the current position to account for the segment's fraction of the total // width (ignoring offset and padding) x += segWidth; + + // No space left to draw the segment + if (start > end) continue; + + if (segment.mDashed) { + // No caps when the segment is dashed. + + mDashedStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor + : mState.mFadedStrokeColor); + canvas.drawLine(start, centerY, end, centerY, mDashedStrokePaint); + } else if (end - start < mState.mStrokeWidth) { + // Not enough segment length to draw the caps + + final float rad = (end - start) / 2F; + final float capWidth = mStrokePaint.getStrokeWidth() / 2F; + + mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor + : mState.mStrokeColor); + + mSegRectF.set(start, centerY - capWidth, end, centerY + capWidth); + canvas.drawRoundRect(mSegRectF, rad, rad, mFillPaint); + } else { + // Leave space for the rounded line cap which extends beyond start/end. + final float capWidth = mStrokePaint.getStrokeWidth() / 2F; + + // Transparent is not allowed (and also is the default in the data), so use that + // as a sentinel to be replaced by default + mStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor + : mState.mStrokeColor); + + canvas.drawLine(start + capWidth, centerY, end - capWidth, centerY, + mStrokePaint); + } } else if (part instanceof Point point) { final float pointWidth = 2 * pointRadius; float start = x - pointRadius; @@ -232,7 +253,7 @@ public final class NotificationProgressDrawable extends Drawable { } else { // TODO: b/367804171 - actually use a vector asset for the default point // rather than drawing it as a box? - mPointRectF.set(mPointRect); + mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); final float inset = mState.mPointRectInset; final float cornerRadius = mState.mPointRectCornerRadius; mPointRectF.inset(inset, inset); diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index 50252c11ffb1..42406147b2f0 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -538,7 +538,7 @@ static bool attributionSourceStateForJavaParcel(JNIEnv *env, jobject jClientAttr return false; } - if (!(useContextAttributionSource && flags::use_context_attribution_source())) { + if (!(useContextAttributionSource && flags::data_delivery_permission_checks())) { clientAttribution.uid = Camera::USE_CALLING_UID; clientAttribution.pid = Camera::USE_CALLING_PID; } diff --git a/core/jni/android_hardware_input_InputWindowHandle.cpp b/core/jni/android_hardware_input_InputWindowHandle.cpp index 69f633420a0d..f1c4913fe006 100644 --- a/core/jni/android_hardware_input_InputWindowHandle.cpp +++ b/core/jni/android_hardware_input_InputWindowHandle.cpp @@ -83,68 +83,53 @@ static struct { jmethodID ctor; } gRegionClassInfo; -static Mutex gHandleMutex; - - -// --- NativeInputWindowHandle --- - -NativeInputWindowHandle::NativeInputWindowHandle(jweak objWeak) : - mObjWeak(objWeak) { -} - -NativeInputWindowHandle::~NativeInputWindowHandle() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); - env->DeleteWeakGlobalRef(mObjWeak); +// --- Global functions --- - // Clear the weak reference to the layer handle and flush any binder ref count operations so we - // do not hold on to any binder references. - // TODO(b/139697085) remove this after it can be flushed automatically - mInfo.touchableRegionCropHandle.clear(); - IPCThreadState::self()->flushCommands(); -} +sp<gui::WindowInfoHandle> android_view_InputWindowHandle_getHandle(JNIEnv* env, jobject obj) { + sp<gui::WindowInfoHandle> handle = [&]() { + jlong cachedHandle = env->GetLongField(obj, gInputWindowHandleClassInfo.ptr); + if (cachedHandle) { + return sp<gui::WindowInfoHandle>::fromExisting( + reinterpret_cast<gui::WindowInfoHandle*>(cachedHandle)); + } -jobject NativeInputWindowHandle::getInputWindowHandleObjLocalRef(JNIEnv* env) { - return env->NewLocalRef(mObjWeak); -} + auto newHandle = sp<gui::WindowInfoHandle>::make(); + newHandle->incStrong((void*)android_view_InputWindowHandle_getHandle); + env->SetLongField(obj, gInputWindowHandleClassInfo.ptr, + reinterpret_cast<jlong>(newHandle.get())); + return newHandle; + }(); -bool NativeInputWindowHandle::updateInfo() { - JNIEnv* env = AndroidRuntime::getJNIEnv(); - jobject obj = env->NewLocalRef(mObjWeak); - if (!obj) { - releaseChannel(); - return false; - } + gui::WindowInfo* windowInfo = handle->editInfo(); - mInfo.touchableRegion.clear(); + windowInfo->touchableRegion.clear(); jobject tokenObj = env->GetObjectField(obj, gInputWindowHandleClassInfo.token); if (tokenObj) { - mInfo.token = ibinderForJavaObject(env, tokenObj); + windowInfo->token = ibinderForJavaObject(env, tokenObj); env->DeleteLocalRef(tokenObj); } else { - mInfo.token.clear(); + windowInfo->token.clear(); } - mInfo.name = getStringField(env, obj, gInputWindowHandleClassInfo.name, "<null>"); + windowInfo->name = getStringField(env, obj, gInputWindowHandleClassInfo.name, "<null>"); - mInfo.dispatchingTimeout = std::chrono::milliseconds( + windowInfo->dispatchingTimeout = std::chrono::milliseconds( env->GetLongField(obj, gInputWindowHandleClassInfo.dispatchingTimeoutMillis)); ScopedLocalRef<jobject> frameObj(env, env->GetObjectField(obj, gInputWindowHandleClassInfo.frame)); - mInfo.frame = JNICommon::rectFromObj(env, frameObj.get()); + windowInfo->frame = JNICommon::rectFromObj(env, frameObj.get()); - mInfo.surfaceInset = env->GetIntField(obj, - gInputWindowHandleClassInfo.surfaceInset); - mInfo.globalScaleFactor = env->GetFloatField(obj, - gInputWindowHandleClassInfo.scaleFactor); + windowInfo->surfaceInset = env->GetIntField(obj, gInputWindowHandleClassInfo.surfaceInset); + windowInfo->globalScaleFactor = + env->GetFloatField(obj, gInputWindowHandleClassInfo.scaleFactor); - jobject regionObj = env->GetObjectField(obj, - gInputWindowHandleClassInfo.touchableRegion); + jobject regionObj = env->GetObjectField(obj, gInputWindowHandleClassInfo.touchableRegion); if (regionObj) { for (graphics::RegionIterator it(env, regionObj); !it.isDone(); it.next()) { ARect rect = it.getRect(); - mInfo.addTouchableRegion(Rect(rect.left, rect.top, rect.right, rect.bottom)); + windowInfo->addTouchableRegion(Rect(rect.left, rect.top, rect.right, rect.bottom)); } env->DeleteLocalRef(regionObj); } @@ -153,49 +138,55 @@ bool NativeInputWindowHandle::updateInfo() { env->GetIntField(obj, gInputWindowHandleClassInfo.layoutParamsFlags)); const auto type = static_cast<WindowInfo::Type>( env->GetIntField(obj, gInputWindowHandleClassInfo.layoutParamsType)); - mInfo.layoutParamsFlags = flags; - mInfo.layoutParamsType = type; + windowInfo->layoutParamsFlags = flags; + windowInfo->layoutParamsType = type; - mInfo.inputConfig = static_cast<gui::WindowInfo::InputConfig>( + windowInfo->inputConfig = static_cast<gui::WindowInfo::InputConfig>( env->GetIntField(obj, gInputWindowHandleClassInfo.inputConfig)); - mInfo.touchOcclusionMode = static_cast<TouchOcclusionMode>( + windowInfo->touchOcclusionMode = static_cast<TouchOcclusionMode>( env->GetIntField(obj, gInputWindowHandleClassInfo.touchOcclusionMode)); - mInfo.ownerPid = gui::Pid{env->GetIntField(obj, gInputWindowHandleClassInfo.ownerPid)}; - mInfo.ownerUid = gui::Uid{ + windowInfo->ownerPid = gui::Pid{env->GetIntField(obj, gInputWindowHandleClassInfo.ownerPid)}; + windowInfo->ownerUid = gui::Uid{ static_cast<uid_t>(env->GetIntField(obj, gInputWindowHandleClassInfo.ownerUid))}; - mInfo.packageName = getStringField(env, obj, gInputWindowHandleClassInfo.packageName, "<null>"); - mInfo.displayId = + windowInfo->packageName = + getStringField(env, obj, gInputWindowHandleClassInfo.packageName, "<null>"); + windowInfo->displayId = ui::LogicalDisplayId{env->GetIntField(obj, gInputWindowHandleClassInfo.displayId)}; - jobject inputApplicationHandleObj = env->GetObjectField(obj, - gInputWindowHandleClassInfo.inputApplicationHandle); + jobject inputApplicationHandleObj = + env->GetObjectField(obj, gInputWindowHandleClassInfo.inputApplicationHandle); if (inputApplicationHandleObj) { std::shared_ptr<InputApplicationHandle> inputApplicationHandle = android_view_InputApplicationHandle_getHandle(env, inputApplicationHandleObj); if (inputApplicationHandle != nullptr) { inputApplicationHandle->updateInfo(); - mInfo.applicationInfo = *(inputApplicationHandle->getInfo()); + windowInfo->applicationInfo = *(inputApplicationHandle->getInfo()); } env->DeleteLocalRef(inputApplicationHandleObj); } - mInfo.replaceTouchableRegionWithCrop = env->GetBooleanField(obj, - gInputWindowHandleClassInfo.replaceTouchableRegionWithCrop); + windowInfo->replaceTouchableRegionWithCrop = + env->GetBooleanField(obj, gInputWindowHandleClassInfo.replaceTouchableRegionWithCrop); - jobject weakSurfaceCtrl = env->GetObjectField(obj, - gInputWindowHandleClassInfo.touchableRegionSurfaceControl.ctrl); + jobject weakSurfaceCtrl = + env->GetObjectField(obj, + gInputWindowHandleClassInfo.touchableRegionSurfaceControl.ctrl); bool touchableRegionCropHandleSet = false; if (weakSurfaceCtrl) { // Promote java weak reference. - jobject strongSurfaceCtrl = env->CallObjectMethod(weakSurfaceCtrl, - gInputWindowHandleClassInfo.touchableRegionSurfaceControl.get); + jobject strongSurfaceCtrl = + env->CallObjectMethod(weakSurfaceCtrl, + gInputWindowHandleClassInfo.touchableRegionSurfaceControl + .get); if (strongSurfaceCtrl) { - jlong mNativeObject = env->GetLongField(strongSurfaceCtrl, - gInputWindowHandleClassInfo.touchableRegionSurfaceControl.mNativeObject); + jlong mNativeObject = + env->GetLongField(strongSurfaceCtrl, + gInputWindowHandleClassInfo.touchableRegionSurfaceControl + .mNativeObject); if (mNativeObject) { auto ctrl = reinterpret_cast<SurfaceControl *>(mNativeObject); - mInfo.touchableRegionCropHandle = ctrl->getHandle(); + windowInfo->touchableRegionCropHandle = ctrl->getHandle(); touchableRegionCropHandleSet = true; } env->DeleteLocalRef(strongSurfaceCtrl); @@ -203,15 +194,15 @@ bool NativeInputWindowHandle::updateInfo() { env->DeleteLocalRef(weakSurfaceCtrl); } if (!touchableRegionCropHandleSet) { - mInfo.touchableRegionCropHandle.clear(); + windowInfo->touchableRegionCropHandle.clear(); } jobject windowTokenObj = env->GetObjectField(obj, gInputWindowHandleClassInfo.windowToken); if (windowTokenObj) { - mInfo.windowToken = ibinderForJavaObject(env, windowTokenObj); + windowInfo->windowToken = ibinderForJavaObject(env, windowTokenObj); env->DeleteLocalRef(windowTokenObj); } else { - mInfo.windowToken.clear(); + windowInfo->windowToken.clear(); } ScopedLocalRef<jobject> @@ -220,41 +211,16 @@ bool NativeInputWindowHandle::updateInfo() { gInputWindowHandleClassInfo .focusTransferTarget)); if (focusTransferTargetObj.get()) { - mInfo.focusTransferTarget = ibinderForJavaObject(env, focusTransferTargetObj.get()); + windowInfo->focusTransferTarget = ibinderForJavaObject(env, focusTransferTargetObj.get()); } else { - mInfo.focusTransferTarget.clear(); + windowInfo->focusTransferTarget.clear(); } - env->DeleteLocalRef(obj); - return true; -} - - -// --- Global functions --- - -sp<NativeInputWindowHandle> android_view_InputWindowHandle_getHandle( - JNIEnv* env, jobject inputWindowHandleObj) { - if (!inputWindowHandleObj) { - return NULL; - } - - AutoMutex _l(gHandleMutex); - - jlong ptr = env->GetLongField(inputWindowHandleObj, gInputWindowHandleClassInfo.ptr); - NativeInputWindowHandle* handle; - if (ptr) { - handle = reinterpret_cast<NativeInputWindowHandle*>(ptr); - } else { - jweak objWeak = env->NewWeakGlobalRef(inputWindowHandleObj); - handle = new NativeInputWindowHandle(objWeak); - handle->incStrong((void*)android_view_InputWindowHandle_getHandle); - env->SetLongField(inputWindowHandleObj, gInputWindowHandleClassInfo.ptr, - reinterpret_cast<jlong>(handle)); - } return handle; } -jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env, gui::WindowInfo windowInfo) { +jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env, + const gui::WindowInfo& windowInfo) { ScopedLocalRef<jobject> applicationHandle(env, android_view_InputApplicationHandle_fromInputApplicationInfo( @@ -337,18 +303,15 @@ jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env, gui::WindowIn // --- JNI --- static void android_view_InputWindowHandle_nativeDispose(JNIEnv* env, jobject obj) { - AutoMutex _l(gHandleMutex); - jlong ptr = env->GetLongField(obj, gInputWindowHandleClassInfo.ptr); - if (ptr) { - env->SetLongField(obj, gInputWindowHandleClassInfo.ptr, 0); - - NativeInputWindowHandle* handle = reinterpret_cast<NativeInputWindowHandle*>(ptr); - handle->decStrong((void*)android_view_InputWindowHandle_getHandle); + if (!ptr) { + return; } + env->SetLongField(obj, gInputWindowHandleClassInfo.ptr, 0); + auto handle = reinterpret_cast<gui::WindowInfoHandle*>(ptr); + handle->decStrong((void*)android_view_InputWindowHandle_getHandle); } - static const JNINativeMethod gInputWindowHandleMethods[] = { /* name, signature, funcPtr */ { "nativeDispose", "()V", diff --git a/core/jni/android_hardware_input_InputWindowHandle.h b/core/jni/android_hardware_input_InputWindowHandle.h index 408e0f1bfa36..aa375e9ef477 100644 --- a/core/jni/android_hardware_input_InputWindowHandle.h +++ b/core/jni/android_hardware_input_InputWindowHandle.h @@ -24,24 +24,11 @@ namespace android { -class NativeInputWindowHandle : public gui::WindowInfoHandle { -public: - NativeInputWindowHandle(jweak objWeak); - virtual ~NativeInputWindowHandle(); +sp<gui::WindowInfoHandle> android_view_InputWindowHandle_getHandle(JNIEnv* env, + jobject inputWindowHandleObj); - jobject getInputWindowHandleObjLocalRef(JNIEnv* env); - - virtual bool updateInfo(); - -private: - jweak mObjWeak; -}; - -extern sp<NativeInputWindowHandle> android_view_InputWindowHandle_getHandle( - JNIEnv* env, jobject inputWindowHandleObj); - -extern jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env, - gui::WindowInfo windowInfo); +jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env, + const gui::WindowInfo& windowInfo); } // namespace android diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 56292c3d0fb2..d3bf36e60345 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -979,14 +979,16 @@ static void nativeSetAlpha(JNIEnv* env, jclass clazz, jlong transactionObj, static void nativeSetInputWindowInfo(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject, jobject inputWindow) { - auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); + if (!inputWindow) { + jniThrowNullPointerException(env, "InputWindowHandle is null"); + return; + } - sp<NativeInputWindowHandle> handle = android_view_InputWindowHandle_getHandle( - env, inputWindow); - handle->updateInfo(); + auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); + sp<gui::WindowInfoHandle> info = android_view_InputWindowHandle_getHandle(env, inputWindow); auto ctrl = reinterpret_cast<SurfaceControl *>(nativeObject); - transaction->setInputWindowInfo(ctrl, *handle->getInfo()); + transaction->setInputWindowInfo(ctrl, std::move(info)); } static void nativeAddWindowInfosReportedListener(JNIEnv* env, jclass clazz, jlong transactionObj, diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 606e829c41fa..6af742fb23f4 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -104,6 +104,7 @@ message SecureSettingsProto { optional SettingProto accessibility_single_finger_panning_enabled = 56 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto accessibility_gesture_targets = 57 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto display_daltonizer_saturation_level = 58 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; diff --git a/core/res/Android.bp b/core/res/Android.bp index c66d2b57fcef..0e4e22b09e24 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -158,6 +158,7 @@ android_app { flags_packages: [ "android.app.appfunctions.flags-aconfig", "android.app.contextualsearch.flags-aconfig", + "android.app.flags-aconfig", "android.appwidget.flags-aconfig", "android.content.pm.flags-aconfig", "android.provider.flags-aconfig", @@ -171,6 +172,7 @@ android_app { "android.security.flags-aconfig", "com.android.hardware.input.input-aconfig", "aconfig_trade_in_mode_flags", + "art-aconfig-flags", "ranging_aconfig_flags", "aconfig_settingslib_flags", ], diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 44bda1c8a877..d09802a91edc 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4463,6 +4463,18 @@ android:description="@string/permdesc_hideOverlayWindows" android:protectionLevel="normal" /> + <!-- Allows an app to enter Picture-in-Picture mode when the user is not explicitly requesting + it. This includes using {@link PictureInPictureParams.Builder#setAutoEnterEnabled} as well + as lifecycle methods such as {@link Activity#onUserLeaveHint} and {@link Activity#onPause} + to enter PiP when the user leaves the app. + This permission should only be used for certain PiP + <a href="{@docRoot}training/tv/get-started/multitasking#usage-types">usage types</a>. + @FlaggedApi(android.app.Flags.FLAG_ENABLE_TV_IMPLICIT_ENTER_PIP_RESTRICTION) + --> + <permission android:name="android.permission.TV_IMPLICIT_ENTER_PIP" + android:protectionLevel="normal" + android:featureFlag="android.app.enable_tv_implicit_enter_pip_restriction" /> + <!-- ================================== --> <!-- Permissions affecting the system wallpaper --> <!-- ================================== --> @@ -8512,6 +8524,16 @@ android:protectionLevel="signature|privileged" android:featureFlag="com.android.tradeinmode.flags.enable_trade_in_mode" /> + <!-- @SystemApi + @FlaggedApi(com.android.art.flags.Flags.FLAG_EXECUTABLE_METHOD_FILE_OFFSETS) + Ability to read program metadata and attach dynamic instrumentation. + <p>Protection level: signature + @hide + --> + <permission android:name="android.permission.DYNAMIC_INSTRUMENTATION" + android:protectionLevel="signature" + android:featureFlag="com.android.art.flags.executable_method_file_offsets" /> + <!-- @TestApi Signature permission reserved for testing. This should never be used to diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 4d73f228ad0c..41dec3776b5c 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -2273,6 +2273,22 @@ <attr name="enableOnBackInvokedCallback" format="boolean"/> <attr name="intentMatchingFlags"/> + + <!-- Specifies the set of drawable resources that can be used in place + of an existing declared icon or banner for activities that appear + in the app launcher. The resource referenced must be an array of + drawable resources and can contain at most 500 items. + {@link android.content.pm.PackageManager#changeLauncherIconConfig} + @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) --> + <attr name="alternateLauncherIcons" format="reference" /> + + <!-- Specifies the set of string resources that can be used in place + of an existing declared label for activities that appear + in the app launcher. The resource referenced must be an array of + string resources and can contain at most 500 items. + {@link android.content.pm.PackageManager#changeLauncherIconConfig} + @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) --> + <attr name="alternateLauncherLabels" format="reference" /> </declare-styleable> <!-- An attribution is a logical part of an app and is identified by a tag. diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 7402a2f24f97..219cefd273ac 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4596,6 +4596,11 @@ exists on the device, the accessibility shortcut will be disabled by default. --> <string name="config_defaultAccessibilityService" translatable="false"></string> + <!-- The component name, flattened to a string, for the default select to speak service to be + enabled by the accessibility keyboard shortcut. If the service with the specified component + name is not preinstalled then this shortcut will do nothing. --> + <string name="config_defaultSelectToSpeakService" translatable="false"></string> + <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. --> <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 70cc5f14391d..b6436d0b30a5 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -127,6 +127,10 @@ <public name="intentMatchingFlags"/> <!-- @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) --> <public name="layoutLabel"/> + <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) --> + <public name="alternateLauncherIcons"/> + <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) --> + <public name="alternateLauncherLabels"/> </staging-public-group> <staging-public-group type="id" first-id="0x01b60000"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index db81a3be440f..badb98686fb2 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3718,6 +3718,7 @@ <java-symbol type="string" name="color_correction_feature_name" /> <java-symbol type="string" name="reduce_bright_colors_feature_name" /> <java-symbol type="string" name="config_defaultAccessibilityService" /> + <java-symbol type="string" name="config_defaultSelectToSpeakService" /> <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" /> <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" /> <java-symbol type="array" name="config_trustedAccessibilityServices" /> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index a382d798fb9b..f39508d6de15 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -151,6 +151,7 @@ android_test { ":HelloWorldUsingSdk1And2", ":HelloWorldUsingSdkMalformedNegativeVersion", ":CtsStaticSharedLibConsumerApp1", + ":CtsStaticSharedLibConsumerApp3", ], } diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml index 3f7c83a82787..5d8ff87eca24 100644 --- a/core/tests/coretests/AndroidTest.xml +++ b/core/tests/coretests/AndroidTest.xml @@ -41,6 +41,8 @@ value="/data/local/tmp/tests/coretests/pm/HelloWorldSdk1.apk"/> <option name="push-file" key="CtsStaticSharedLibConsumerApp1.apk" value="/data/local/tmp/tests/coretests/pm/CtsStaticSharedLibConsumerApp1.apk"/> + <option name="push-file" key="CtsStaticSharedLibConsumerApp3.apk" + value="/data/local/tmp/tests/coretests/pm/CtsStaticSharedLibConsumerApp3.apk"/> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index da1fffa0fac4..a2598f69e031 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -16,6 +16,7 @@ package android.app; +import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID; import static android.app.PropertyInvalidatedCache.NONCE_UNSET; import static android.app.PropertyInvalidatedCache.MODULE_BLUETOOTH; import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM; @@ -30,8 +31,9 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import android.app.PropertyInvalidatedCache.Args; import android.annotation.SuppressLint; +import android.app.PropertyInvalidatedCache.Args; +import android.os.Binder; import com.android.internal.os.ApplicationSharedMemory; import android.platform.test.annotations.IgnoreUnderRavenwood; @@ -58,6 +60,7 @@ import org.junit.Test; */ @SmallTest public class PropertyInvalidatedCacheTests { + @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -455,8 +458,9 @@ public class PropertyInvalidatedCacheTests { // Test the Args-style constructor. @Test public void testArgsConstructor() { - // Create a cache with a maximum of four entries. - TestCache cache = new TestCache(new Args(MODULE_TEST).api("init1").maxEntries(4), + // Create a cache with a maximum of four entries and non-isolated UIDs. + TestCache cache = new TestCache(new Args(MODULE_TEST) + .maxEntries(4).isolateUids(false).api("init1"), new TestQuery()); cache.invalidateCache(); @@ -570,4 +574,73 @@ public class PropertyInvalidatedCacheTests { // Expected exception. } } + + // Verify that a cache created with isolatedUids(true) separates out the results. + @RequiresFlagsEnabled(FLAG_PIC_ISOLATE_CACHE_BY_UID) + @Test + public void testIsolatedUids() { + TestCache cache = new TestCache(new Args(MODULE_TEST) + .maxEntries(4).isolateUids(true).api("testIsolatedUids").testMode(true), + new TestQuery()); + cache.invalidateCache(); + final int uid1 = 1; + final int uid2 = 2; + + long token = Binder.setCallingWorkSourceUid(uid1); + try { + // Populate the cache for user 1 + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo6", cache.query(6)); + assertEquals(2, cache.getRecomputeCount()); + + // Populate the cache for user 2. User 1 values are not reused. + Binder.setCallingWorkSourceUid(uid2); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + + // Verify that the cache for user 1 is still populated. + Binder.setCallingWorkSourceUid(uid1); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + + } finally { + Binder.restoreCallingWorkSource(token); + } + + // Repeat the test with a non-isolated cache. + cache = new TestCache(new Args(MODULE_TEST) + .maxEntries(4).isolateUids(false).api("testIsolatedUids2").testMode(true), + new TestQuery()); + cache.invalidateCache(); + token = Binder.setCallingWorkSourceUid(uid1); + try { + // Populate the cache for user 1 + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo6", cache.query(6)); + assertEquals(2, cache.getRecomputeCount()); + + // Populate the cache for user 2. User 1 values are reused. + Binder.setCallingWorkSourceUid(uid2); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + + // Verify that the cache for user 1 is still populated. + Binder.setCallingWorkSourceUid(uid1); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + + } finally { + Binder.restoreCallingWorkSource(token); + } + } } diff --git a/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java b/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java index d4618d744644..0db49a72c51d 100644 --- a/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java +++ b/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java @@ -72,6 +72,12 @@ public class ApkLiteParseUtilsTest { private static final String TEST_APP_USING_SDK_MALFORMED_VERSION = "HelloWorldUsingSdkMalformedNegativeVersion.apk"; private static final String TEST_APP_USING_STATIC_LIB = "CtsStaticSharedLibConsumerApp1.apk"; + private static final String TEST_APP_USING_STATIC_LIB_TWO_CERTS = + "CtsStaticSharedLibConsumerApp3.apk"; + private static final String STATIC_LIB_CERT_1 = + "70fbd440503ec0bf41f3f21fcc83ffd39880133c27deb0945ed677c6f31d72fb"; + private static final String STATIC_LIB_CERT_2 = + "e49582ff3a0aa4c5589fc5feaac6b7d6e757199dd0c6742df7bf37c2ffef95f5"; private static final String TEST_SDK1 = "HelloWorldSdk1.apk"; private static final String TEST_SDK1_PACKAGE = "com.test.sdk1_1"; private static final String TEST_SDK1_NAME = "com.test.sdk1"; @@ -86,7 +92,7 @@ public class ApkLiteParseUtilsTest { @Before public void setUp() throws IOException { - mTmpDir = mTemporaryFolder.newFolder("DexMetadataHelperTest"); + mTmpDir = mTemporaryFolder.newFolder("ApkLiteParseUtilsTest"); } @After @@ -108,9 +114,8 @@ public class ApkLiteParseUtilsTest { assertThat(baseApk.getUsesSdkLibrariesVersionsMajor()).asList().containsExactly( TEST_SDK1_VERSION, TEST_SDK2_VERSION ); - for (String[] certDigests: baseApk.getUsesSdkLibrariesCertDigests()) { - assertThat(certDigests).asList().containsExactly(""); - } + String[][] expectedCerts = {{""}, {""}}; + assertThat(baseApk.getUsesSdkLibrariesCertDigests()).isEqualTo(expectedCerts); } @SuppressLint("CheckResult") @@ -126,18 +131,13 @@ public class ApkLiteParseUtilsTest { ApkLite baseApk = result.getResult(); String[][] liteCerts = baseApk.getUsesSdkLibrariesCertDigests(); - assertThat(liteCerts).isNotNull(); - for (String[] certDigests: liteCerts) { - assertThat(certDigests).asList().containsExactly(certDigest); - } + String[][] expectedCerts = {{certDigest}, {certDigest}}; + assertThat(liteCerts).isEqualTo(expectedCerts); // Same for package parser AndroidPackage pkg = mPackageParser2.parsePackage(apkFile, 0, true).hideAsFinal(); String[][] pkgCerts = pkg.getUsesSdkLibrariesCertDigests(); - assertThat(pkgCerts).isNotNull(); - for (int i = 0; i < liteCerts.length; i++) { - assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]); - } + assertThat(liteCerts).isEqualTo(pkgCerts); } @@ -160,9 +160,7 @@ public class ApkLiteParseUtilsTest { String[][] liteCerts = baseApk.getUsesSdkLibrariesCertDigests(); String[][] pkgCerts = pkg.getUsesSdkLibrariesCertDigests(); - for (int i = 0; i < liteCerts.length; i++) { - assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]); - } + assertThat(liteCerts).isEqualTo(pkgCerts); } @SuppressLint("CheckResult") @@ -184,9 +182,27 @@ public class ApkLiteParseUtilsTest { String[][] liteCerts = baseApk.getUsesStaticLibrariesCertDigests(); String[][] pkgCerts = pkg.getUsesStaticLibrariesCertDigests(); - for (int i = 0; i < liteCerts.length; i++) { - assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]); - } + assertThat(liteCerts).isEqualTo(pkgCerts); + } + + @Test + public void testParseApkLite_getUsesStaticLibrary_twoCerts() + throws Exception { + File apkFile = copyApkToTmpDir(TEST_APP_USING_STATIC_LIB_TWO_CERTS); + ParseResult<ApkLite> result = ApkLiteParseUtils + .parseApkLite(ParseTypeImpl.forDefaultParsing().reset(), apkFile, 0); + assertThat(result.isError()).isFalse(); + ApkLite baseApk = result.getResult(); + + // There are two certs. + String[][] expectedCerts = {{STATIC_LIB_CERT_1, STATIC_LIB_CERT_2}}; + String[][] liteCerts = baseApk.getUsesStaticLibrariesCertDigests(); + assertThat(liteCerts).isEqualTo(expectedCerts); + + // And they are same as package parser. + AndroidPackage pkg = mPackageParser2.parsePackage(apkFile, 0, true).hideAsFinal(); + String[][] pkgCerts = pkg.getUsesStaticLibrariesCertDigests(); + assertThat(liteCerts).isEqualTo(pkgCerts); } @SuppressLint("CheckResult") diff --git a/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java b/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java index 8bebc62e93f2..1a9af6b55eed 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java @@ -21,6 +21,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_MAG import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.google.common.truth.Truth.assertThat; @@ -123,6 +124,14 @@ public class ShortcutUtilsTest { } @Test + public void getShortcutTargets_keyGestureShortcutNoService_emptyResult() { + assertThat( + ShortcutUtils.getShortcutTargetsFromSettings( + mContext, KEY_GESTURE, mDefaultUserId) + ).isEmpty(); + } + + @Test public void getShortcutTargets_softwareShortcut1Service_return1Service() { setupShortcutTargets(ONE_COMPONENT, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); setupShortcutTargets(TWO_COMPONENTS, Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); diff --git a/data/etc/platform.xml b/data/etc/platform.xml index 7b96699f7f71..857df1038df1 100644 --- a/data/etc/platform.xml +++ b/data/etc/platform.xml @@ -213,6 +213,8 @@ <assign-permission name="android.permission.REGISTER_STATS_PULL_ATOM" uid="gpu_service" /> <assign-permission name="android.permission.REGISTER_STATS_PULL_ATOM" uid="keystore" /> + <assign-permission name="android.permission.DYNAMIC_INSTRUMENTATION" uid="uprobestats" /> + <split-permission name="android.permission.ACCESS_FINE_LOCATION"> <new-permission name="android.permission.ACCESS_COARSE_LOCATION" /> </split-permission> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 7ced809d2a3a..541ca602a386 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -594,6 +594,9 @@ applications that come with the platform <!-- Permission required for CTS test - AdvancedProtectionManagerTest --> <permission name="android.permission.SET_ADVANCED_PROTECTION_MODE" /> <permission name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" /> + <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest --> + <permission name="android.permission.READ_SYSTEM_PREFERENCES" /> + <permission name="android.permission.WRITE_SYSTEM_PREFERENCES" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl index f8d3bffbe00b..2b0802b54c14 100644 --- a/data/keyboards/Generic.kl +++ b/data/keyboards/Generic.kl @@ -171,7 +171,7 @@ key 143 WAKEUP # key 149 "KEY_PROG2" key 150 EXPLORER # key 151 "KEY_MSDOS" -key 152 POWER +key 152 LOCK # key 153 "KEY_DIRECTION" # key 154 "KEY_CYCLEWINDOWS" key 155 ENVELOPE @@ -200,20 +200,20 @@ key 177 PAGE_UP key 178 PAGE_DOWN key 179 NUMPAD_LEFT_PAREN key 180 NUMPAD_RIGHT_PAREN -# key 181 "KEY_NEW" +key 181 NEW # key 182 "KEY_REDO" -# key 183 F13 -# key 184 F14 -# key 185 F15 -# key 186 F16 -# key 187 F17 -# key 188 F18 -# key 189 F19 -# key 190 F20 -# key 191 F21 -# key 192 F22 -# key 193 F23 -# key 194 F24 +key 183 F13 +key 184 F14 +key 185 F15 +key 186 F16 +key 187 F17 +key 188 F18 +key 189 F19 +key 190 F20 +key 191 F21 +key 192 F22 +key 193 F23 +key 194 F24 # key 195 (undefined) # key 196 (undefined) # key 197 (undefined) @@ -225,11 +225,11 @@ key 201 MEDIA_PAUSE # key 203 "KEY_PROG4" key 204 NOTIFICATION # key 205 "KEY_SUSPEND" -# key 206 "KEY_CLOSE" +key 206 CLOSE key 207 MEDIA_PLAY key 208 MEDIA_FAST_FORWARD # key 209 "KEY_BASSBOOST" -# key 210 "KEY_PRINT" +key 210 PRINT # key 211 "KEY_HP" key 212 CAMERA key 213 MUSIC @@ -328,7 +328,7 @@ key 368 LANGUAGE_SWITCH # key 369 "KEY_TITLE" key 370 CAPTIONS # key 371 "KEY_ANGLE" -# key 372 "KEY_ZOOM" +key 372 FULLSCREEN # key 373 "KEY_MODE" # key 374 "KEY_KEYBOARD" # key 375 "KEY_SCREEN" @@ -425,12 +425,15 @@ key 582 VOICE_ASSIST # Linux KEY_ASSISTANT key 583 ASSIST key 585 EMOJI_PICKER +key 586 DICTATE key 656 MACRO_1 key 657 MACRO_2 key 658 MACRO_3 key 659 MACRO_4 # Keys defined by HID usages +key usage 0x010082 LOCK FALLBACK_USAGE_MAPPING +key usage 0x01009B DO_NOT_DISTURB FALLBACK_USAGE_MAPPING key usage 0x0c0065 SCREENSHOT FALLBACK_USAGE_MAPPING key usage 0x0c0067 WINDOW FALLBACK_USAGE_MAPPING key usage 0x0c006F BRIGHTNESS_UP FALLBACK_USAGE_MAPPING @@ -438,12 +441,17 @@ key usage 0x0c0070 BRIGHTNESS_DOWN FALLBACK_USAGE_MAPPING key usage 0x0c0079 KEYBOARD_BACKLIGHT_UP FALLBACK_USAGE_MAPPING key usage 0x0c007A KEYBOARD_BACKLIGHT_DOWN FALLBACK_USAGE_MAPPING key usage 0x0c007C KEYBOARD_BACKLIGHT_TOGGLE FALLBACK_USAGE_MAPPING +key usage 0x0c00D8 DICTATE FALLBACK_USAGE_MAPPING key usage 0x0c00D9 EMOJI_PICKER FALLBACK_USAGE_MAPPING key usage 0x0c0173 MEDIA_AUDIO_TRACK FALLBACK_USAGE_MAPPING key usage 0x0c019C PROFILE_SWITCH FALLBACK_USAGE_MAPPING key usage 0x0c019F SETTINGS FALLBACK_USAGE_MAPPING key usage 0x0c01A2 ALL_APPS FALLBACK_USAGE_MAPPING +key usage 0x0c0201 NEW FALLBACK_USAGE_MAPPING +key usage 0x0c0203 CLOSE FALLBACK_USAGE_MAPPING +key usage 0x0c0208 PRINT FALLBACK_USAGE_MAPPING key usage 0x0c0227 REFRESH FALLBACK_USAGE_MAPPING +key usage 0x0c0232 FULLSCREEN FALLBACK_USAGE_MAPPING key usage 0x0c029D LANGUAGE_SWITCH FALLBACK_USAGE_MAPPING key usage 0x0c029F RECENT_APPS FALLBACK_USAGE_MAPPING key usage 0x0c02A2 ALL_APPS FALLBACK_USAGE_MAPPING diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java index 4c47de0ca754..d55a71e21931 100644 --- a/graphics/java/android/graphics/ColorSpace.java +++ b/graphics/java/android/graphics/ColorSpace.java @@ -1761,7 +1761,7 @@ public abstract class ColorSpace { if (Flags.displayBt2020Colorspace()) { sNamedColorSpaceMap.put(Named.DISPLAY_BT2020.ordinal(), new ColorSpace.Rgb( - "BT 2020", + "Display BT. 2020", BT2020_PRIMARIES, ILLUMINANT_D65, null, diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 8bb32568ec5a..56bb0f0d12d5 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -2119,7 +2119,7 @@ public class Paint { * @see FontVariationAxis */ public boolean setFontVariationSettings(String fontVariationSettings) { - final boolean useFontVariationStore = Flags.typefaceRedesign() + final boolean useFontVariationStore = Flags.typefaceRedesignReadonly() && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT); if (useFontVariationStore) { FontVariationAxis[] axes = diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java index ed17fdefcb53..43216ba6e087 100644 --- a/graphics/java/android/graphics/text/PositionedGlyphs.java +++ b/graphics/java/android/graphics/text/PositionedGlyphs.java @@ -133,7 +133,7 @@ public final class PositionedGlyphs { @NonNull public Font getFont(@IntRange(from = 0) int index) { Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index"); - if (Flags.typefaceRedesign()) { + if (Flags.typefaceRedesignReadonly()) { return mFonts.get(nGetFontId(mLayoutPtr, index)); } return mFonts.get(index); @@ -252,7 +252,7 @@ public final class PositionedGlyphs { mXOffset = xOffset; mYOffset = yOffset; - if (Flags.typefaceRedesign()) { + if (Flags.typefaceRedesignReadonly()) { int fontCount = nGetFontCount(layoutPtr); mFonts = new ArrayList<>(fontCount); for (int i = 0; i < fontCount; ++i) { diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 0b515f590f98..5f42bb161204 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -475,6 +475,6 @@ class BubbleStackViewTest { override fun hideCurrentInputMethod() {} - override fun updateBubbleBarLocation(location: BubbleBarLocation) {} + override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) {} } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt index 0d742cc6e382..6ac36a3319c9 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt @@ -375,7 +375,7 @@ class BubbleBarExpandedViewTest { override fun hideCurrentInputMethod() { } - override fun updateBubbleBarLocation(location: BubbleBarLocation) { + override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) { } } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt index 00d9a931cebe..0044593ad228 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -351,7 +351,7 @@ class BubbleBarLayerViewTest { override fun hideCurrentInputMethod() {} - override fun updateBubbleBarLocation(location: BubbleBarLocation) {} + override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) {} } } diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml index 501bedd50f55..c2755ef6ccb6 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml @@ -19,6 +19,7 @@ android:layout_height="wrap_content" android:layout_width="wrap_content" android:orientation="vertical" + android:clipChildren="false" android:id="@+id/bubble_expanded_view"> <com.android.wm.shell.bubbles.bar.BubbleBarHandleView diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml index f1ecde49ce78..7aca921dccc7 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml @@ -14,20 +14,18 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<com.android.wm.shell.bubbles.bar.BubbleBarMenuView - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" +<com.android.wm.shell.bubbles.bar.BubbleBarMenuView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" + android:clipToPadding="false" android:minWidth="@dimen/bubble_bar_manage_menu_min_width" android:orientation="vertical" - android:elevation="@dimen/bubble_manage_menu_elevation" - android:paddingTop="@dimen/bubble_bar_manage_menu_padding_top" - android:paddingHorizontal="@dimen/bubble_bar_manage_menu_padding" - android:paddingBottom="@dimen/bubble_bar_manage_menu_padding" - android:clipToPadding="false"> + android:visibility="invisible" + tools:visibility="visible"> <LinearLayout android:id="@+id/bubble_bar_manage_menu_bubble_section" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index f90e165ffc74..a18a2510f0f7 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -168,7 +168,7 @@ </LinearLayout> <LinearLayout - android:id="@+id/open_in_browser_pill" + android:id="@+id/open_in_app_or_browser_pill" android:layout_width="match_parent" android:layout_height="@dimen/desktop_mode_handle_menu_open_in_browser_pill_height" android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" @@ -178,7 +178,7 @@ android:background="@drawable/desktop_mode_decor_handle_menu_background"> <Button - android:id="@+id/open_in_browser_button" + android:id="@+id/open_in_app_or_browser_button" android:layout_weight="1" android:contentDescription="@string/open_in_browser_text" android:text="@string/open_in_browser_text" diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 8f1ef6c7e49e..012579a6d40c 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -301,6 +301,8 @@ <string name="screenshot_text">Screenshot</string> <!-- Accessibility text for the handle menu open in browser button [CHAR LIMIT=NONE] --> <string name="open_in_browser_text">Open in browser</string> + <!-- Accessibility text for the handle menu open in app button [CHAR LIMIT=NONE] --> + <string name="open_in_app_text">Open in App</string> <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> <string name="new_window_text">New Window</string> <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt index 191875d38daf..84a22b873aaf 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt @@ -15,6 +15,7 @@ */ package com.android.wm.shell.shared.bubbles +import android.annotation.IntDef import android.os.Parcel import android.os.Parcelable @@ -60,4 +61,36 @@ enum class BubbleBarLocation : Parcelable { override fun newArray(size: Int) = arrayOfNulls<BubbleBarLocation>(size) } } + + /** Define set of constants that allow to determine why location changed. */ + @IntDef( + UpdateSource.DRAG_BAR, + UpdateSource.DRAG_BUBBLE, + UpdateSource.DRAG_EXP_VIEW, + UpdateSource.A11Y_ACTION_BAR, + UpdateSource.A11Y_ACTION_BUBBLE, + UpdateSource.A11Y_ACTION_EXP_VIEW, + ) + @Retention(AnnotationRetention.SOURCE) + annotation class UpdateSource { + companion object { + /** Location changed from dragging the bar */ + const val DRAG_BAR = 1 + + /** Location changed from dragging the bubble */ + const val DRAG_BUBBLE = 2 + + /** Location changed from dragging the expanded view */ + const val DRAG_EXP_VIEW = 3 + + /** Location changed via a11y action on the bar */ + const val A11Y_ACTION_BAR = 4 + + /** Location changed via a11y action on the bubble */ + const val A11Y_ACTION_BUBBLE = 5 + + /** Location changed via a11y action on the expanded view */ + const val A11Y_ACTION_EXP_VIEW = 6 + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt index 65132fe89063..7243ea36b137 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt @@ -20,7 +20,9 @@ package com.android.wm.shell.apptoweb import android.content.Context import android.content.Intent +import android.content.Intent.ACTION_VIEW import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER import android.content.pm.PackageManager import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.verify.domain.DomainVerificationUserState @@ -31,7 +33,7 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup private const val TAG = "AppToWebUtils" private val GenericBrowserIntent = Intent() - .setAction(Intent.ACTION_VIEW) + .setAction(ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) .setData(Uri.parse("http:")) @@ -67,6 +69,20 @@ fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? { } /** + * Returns intent if there is a non-browser application available to handle the uri. Otherwise, + * returns null. + */ +fun getAppIntent(uri: Uri, packageManager: PackageManager): Intent? { + val intent = Intent(ACTION_VIEW, uri).apply { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_REQUIRE_NON_BROWSER + } + // If there is no application available to handle intent, return null + val component = intent.resolveActivity(packageManager) ?: return null + intent.setComponent(component) + return intent +} + +/** * Returns the [DomainVerificationUserState] of the user associated with the given * [DomainVerificationManager] and the given package. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index c92a2786e49b..ce7a97703f44 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -1122,7 +1122,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont final BackMotionEvent backFinish = mCurrentTracker .createProgressEvent(); dispatchOnBackProgressed(mActiveCallback, backFinish); - if (!mBackGestureStarted) { + if (mCurrentTracker.isFinished()) { // if the down -> up gesture happened before animation // start, we have to trigger the uninterruptible transition // to finish the back animation. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 14f8cc74bfc5..0fd98ed7eaf1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -740,8 +740,10 @@ public class BubbleController implements ConfigurationChangeListener, /** * Update bubble bar location and trigger and update to listeners */ - public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { + public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, + @BubbleBarLocation.UpdateSource int source) { if (canShowAsBubbleBar()) { + BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation(); mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); if (mLayerView != null && !mLayerView.isExpandedViewDragged()) { mLayerView.updateExpandedView(); @@ -749,13 +751,47 @@ public class BubbleController implements ConfigurationChangeListener, BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); + + logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source); + } + } + + private void logBubbleBarLocationIfChanged(BubbleBarLocation location, + BubbleBarLocation previous, + @BubbleBarLocation.UpdateSource int source) { + if (mLayerView == null) { + return; + } + boolean isRtl = mLayerView.isLayoutRtl(); + boolean wasLeft = previous.isOnLeft(isRtl); + boolean onLeft = location.isOnLeft(isRtl); + if (wasLeft == onLeft) { + // No changes, skip logging + return; + } + switch (source) { + case BubbleBarLocation.UpdateSource.DRAG_BAR: + case BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR: + mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BAR + : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BAR); + break; + case BubbleBarLocation.UpdateSource.DRAG_BUBBLE: + case BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE: + mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BUBBLE + : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BUBBLE); + break; + case BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW: + case BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW: + // TODO(b/349845968): move logging from BubbleBarLayerView to here + break; } } /** * Animate bubble bar to the given location. The location change is transient. It does not * update the state of the bubble bar. - * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}. + * To update bubble bar pinned location, use + * {@link #setBubbleBarLocation(BubbleBarLocation, int)}. */ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { if (canShowAsBubbleBar()) { @@ -2568,9 +2604,10 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void setBubbleBarLocation(BubbleBarLocation location) { + public void setBubbleBarLocation(BubbleBarLocation location, + @BubbleBarLocation.UpdateSource int source) { mMainExecutor.execute(() -> - mController.setBubbleBarLocation(location)); + mController.setBubbleBarLocation(location, source)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index ec4854b47aff..6423eed59165 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -32,7 +32,10 @@ interface BubbleExpandedViewManager { fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean fun hideCurrentInputMethod() - fun updateBubbleBarLocation(location: BubbleBarLocation) + fun updateBubbleBarLocation( + location: BubbleBarLocation, + @BubbleBarLocation.UpdateSource source: Int, + ) companion object { /** @@ -82,8 +85,11 @@ interface BubbleExpandedViewManager { controller.hideCurrentInputMethod() } - override fun updateBubbleBarLocation(location: BubbleBarLocation) { - controller.bubbleBarLocation = location + override fun updateBubbleBarLocation( + location: BubbleBarLocation, + @BubbleBarLocation.UpdateSource source: Int, + ) { + controller.setBubbleBarLocation(location, source) } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 1855b938f48e..9c2d35431554 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -44,7 +44,7 @@ interface IBubbles { oneway void showUserEducation(in int positionX, in int positionY) = 8; - oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9; + oneway void setBubbleBarLocation(in BubbleBarLocation location, in int source) = 9; oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 2a50e4d0d74b..3764bcd42ac6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -222,7 +222,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate()); } - mMenuViewController = new BubbleBarMenuViewController(mContext, this); + mMenuViewController = new BubbleBarMenuViewController(mContext, mHandleView, this); mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() { @Override public void onMenuVisibilityChanged(boolean visible) { @@ -637,11 +637,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView return true; } if (action == R.id.action_move_bubble_bar_left) { - mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT); + mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW); return true; } if (action == R.id.action_move_bubble_bar_right) { - mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT); + mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT, + BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW); return true; } return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java index e781c07f01a7..712e41b0b3c5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java @@ -17,17 +17,18 @@ package com.android.wm.shell.bubbles.bar; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.annotation.Nullable; import android.content.Context; -import android.graphics.Outline; -import android.graphics.Path; -import android.graphics.RectF; +import android.graphics.Canvas; +import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; -import android.view.ViewOutlineProvider; import androidx.annotation.ColorInt; +import androidx.annotation.VisibleForTesting; +import androidx.core.animation.IntProperty; import androidx.core.content.ContextCompat; import com.android.wm.shell.R; @@ -37,14 +38,33 @@ import com.android.wm.shell.R; */ public class BubbleBarHandleView extends View { private static final long COLOR_CHANGE_DURATION = 120; - // Path used to draw the dots - private final Path mPath = new Path(); + /** Custom property to set handle color. */ + private static final IntProperty<BubbleBarHandleView> HANDLE_COLOR = new IntProperty<>( + "handleColor") { + @Override + public void setValue(BubbleBarHandleView bubbleBarHandleView, int color) { + bubbleBarHandleView.setHandleColor(color); + } + + @Override + public Integer get(BubbleBarHandleView bubbleBarHandleView) { + return bubbleBarHandleView.getHandleColor(); + } + }; + + @VisibleForTesting + final Paint mHandlePaint = new Paint(); private final @ColorInt int mHandleLightColor; private final @ColorInt int mHandleDarkColor; - private @ColorInt int mCurrentColor; + private final ArgbEvaluator mArgbEvaluator = ArgbEvaluator.getInstance(); + private final float mHandleHeight; + private final float mHandleWidth; + private float mCurrentHandleHeight; + private float mCurrentHandleWidth; @Nullable private ObjectAnimator mColorChangeAnim; + private @ColorInt int mRegionSamplerColor; public BubbleBarHandleView(Context context) { this(context, null /* attrs */); @@ -61,30 +81,52 @@ public class BubbleBarHandleView extends View { public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - final int handleHeight = getResources().getDimensionPixelSize( + mHandlePaint.setFlags(Paint.ANTI_ALIAS_FLAG); + mHandlePaint.setStyle(Paint.Style.FILL); + mHandlePaint.setColor(0); + mHandleHeight = getResources().getDimensionPixelSize( R.dimen.bubble_bar_expanded_view_handle_height); + mHandleWidth = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_caption_width); mHandleLightColor = ContextCompat.getColor(getContext(), R.color.bubble_bar_expanded_view_handle_light); mHandleDarkColor = ContextCompat.getColor(getContext(), R.color.bubble_bar_expanded_view_handle_dark); - - setClipToOutline(true); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - final int handleCenterY = view.getHeight() / 2; - final int handleTop = handleCenterY - handleHeight / 2; - final int handleBottom = handleTop + handleHeight; - final int radius = handleHeight / 2; - RectF handle = new RectF(/* left = */ 0, handleTop, view.getWidth(), handleBottom); - mPath.reset(); - mPath.addRoundRect(handle, radius, radius, Path.Direction.CW); - outline.setPath(mPath); - } - }); + mCurrentHandleHeight = mHandleHeight; + mCurrentHandleWidth = mHandleWidth; setContentDescription(getResources().getString(R.string.handle_text)); } + private void setHandleColor(int color) { + mHandlePaint.setColor(color); + invalidate(); + } + + private int getHandleColor() { + return mHandlePaint.getColor(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + float handleLeft = (getWidth() - mCurrentHandleWidth) / 2; + float handleRight = handleLeft + mCurrentHandleWidth; + float handleCenterY = (float) getHeight() / 2; + float handleTop = (int) (handleCenterY - mCurrentHandleHeight / 2); + float handleBottom = handleTop + mCurrentHandleHeight; + float cornerRadius = mCurrentHandleHeight / 2; + canvas.drawRoundRect(handleLeft, handleTop, handleRight, handleBottom, cornerRadius, + cornerRadius, mHandlePaint); + } + + /** Sets handle width, height and color. Does not change the layout properties */ + private void setHandleProperties(float width, float height, int color) { + mCurrentHandleHeight = height; + mCurrentHandleWidth = width; + mHandlePaint.setColor(color); + invalidate(); + } + /** * Updates the handle color. * @@ -94,15 +136,15 @@ public class BubbleBarHandleView extends View { */ public void updateHandleColor(boolean isRegionDark, boolean animated) { int newColor = isRegionDark ? mHandleLightColor : mHandleDarkColor; - if (newColor == mCurrentColor) { + if (newColor == mRegionSamplerColor) { return; } + mRegionSamplerColor = newColor; if (mColorChangeAnim != null) { mColorChangeAnim.cancel(); } - mCurrentColor = newColor; if (animated) { - mColorChangeAnim = ObjectAnimator.ofArgb(this, "backgroundColor", newColor); + mColorChangeAnim = ObjectAnimator.ofArgb(this, HANDLE_COLOR, newColor); mColorChangeAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -112,7 +154,39 @@ public class BubbleBarHandleView extends View { mColorChangeAnim.setDuration(COLOR_CHANGE_DURATION); mColorChangeAnim.start(); } else { - setBackgroundColor(newColor); + setHandleColor(newColor); } } + + /** Returns handle padding top. */ + public int getHandlePaddingTop() { + return (getHeight() - getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_handle_height)) / 2; + } + + /** Animates handle for the bubble menu. */ + public void animateHandleForMenu(float progress, float widthDelta, float heightDelta, + int menuColor) { + float currentWidth = mHandleWidth + widthDelta * progress; + float currentHeight = mHandleHeight + heightDelta * progress; + int color = (int) mArgbEvaluator.evaluate(progress, mRegionSamplerColor, menuColor); + setHandleProperties(currentWidth, currentHeight, color); + setTranslationY(heightDelta * progress / 2); + } + + /** Restores all the properties that were animated to the default values. */ + public void restoreAnimationDefaults() { + setHandleProperties(mHandleWidth, mHandleHeight, mRegionSamplerColor); + setTranslationY(0); + } + + /** Returns the handle height. */ + public int getHandleHeight() { + return (int) mHandleHeight; + } + + /** Returns the handle width. */ + public int getHandleWidth() { + return (int) mHandleWidth; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 999ce17905ef..0c05e3c5115c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles.bar; import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN; import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_OUT; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_GESTURE; +import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import android.annotation.Nullable; import android.content.Context; @@ -66,8 +67,6 @@ public class BubbleBarLayerView extends FrameLayout private static final String TAG = BubbleBarLayerView.class.getSimpleName(); - private static final float SCRIM_ALPHA = 0.2f; - private final BubbleController mBubbleController; private final BubbleData mBubbleData; private final BubblePositioner mPositioner; @@ -386,7 +385,7 @@ public class BubbleBarLayerView extends FrameLayout if (show) { mScrimView.animate() .setInterpolator(ALPHA_IN) - .alpha(SCRIM_ALPHA) + .alpha(BUBBLE_EXPANDED_SCRIM_ALPHA) .start(); } else { mScrimView.animate() @@ -442,7 +441,8 @@ public class BubbleBarLayerView extends FrameLayout @Override public void onRelease(@NonNull BubbleBarLocation location) { - mBubbleController.setBubbleBarLocation(location); + mBubbleController.setBubbleBarLocation(location, + BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW); if (location != mInitialLocation) { BubbleLogger.Event event = location.isOnLeft(isLayoutRtl()) ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_EXP_VIEW diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java index 0ee20ef1731f..99e20097e61c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java @@ -47,6 +47,10 @@ public class BubbleBarMenuView extends LinearLayout { private ImageView mBubbleIconView; private ImageView mBubbleDismissIconView; private TextView mBubbleTitleView; + // The animation has three stages. Each stage transition lasts until the animation ends. In + // stage 1, the title item content fades in. In stage 2, the background of the option items + // fades in. In stage 3, the option item content fades in. + private static final int SHOW_MENU_STAGES_COUNT = 3; public BubbleBarMenuView(Context context) { this(context, null /* attrs */); @@ -97,6 +101,35 @@ public class BubbleBarMenuView extends LinearLayout { } } + /** Animates the menu from the specified start scale. */ + public void animateFromStartScale(float currentScale, float progress) { + int menuItemElevation = getResources().getDimensionPixelSize( + R.dimen.bubble_manage_menu_elevation); + setScaleX(currentScale); + setScaleY(currentScale); + setAlphaForTitleViews(progress); + mBubbleSectionView.setElevation(menuItemElevation * progress); + float actionsBackgroundAlpha = Math.max(0, + (progress - (float) 1 / SHOW_MENU_STAGES_COUNT) * (SHOW_MENU_STAGES_COUNT - 1)); + float actionItemsAlpha = Math.max(0, + (progress - (float) 2 / SHOW_MENU_STAGES_COUNT) * SHOW_MENU_STAGES_COUNT); + mActionsSectionView.setAlpha(actionsBackgroundAlpha); + mActionsSectionView.setElevation(menuItemElevation * actionsBackgroundAlpha); + setMenuItemViewsAlpha(actionItemsAlpha); + } + + private void setAlphaForTitleViews(float alpha) { + mBubbleIconView.setAlpha(alpha); + mBubbleTitleView.setAlpha(alpha); + mBubbleDismissIconView.setAlpha(alpha); + } + + private void setMenuItemViewsAlpha(float alpha) { + for (int i = mActionsSectionView.getChildCount() - 1; i >= 0; i--) { + mActionsSectionView.getChildAt(i).setAlpha(alpha); + } + } + /** Update menu details with bubble info */ void updateInfo(Bubble bubble) { if (bubble.getIcon() != null) { @@ -153,6 +186,11 @@ public class BubbleBarMenuView extends LinearLayout { return mBubbleSectionView.getAlpha(); } + /** Return title menu item height. */ + public float getTitleItemHeight() { + return mBubbleSectionView.getHeight(); + } + /** * Menu action details used to create menu items */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 514810745e10..9dd0cae20370 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -15,6 +15,9 @@ */ package com.android.wm.shell.bubbles.bar; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -26,13 +29,10 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.SpringForce; - +import com.android.app.animation.Interpolators; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; -import com.android.wm.shell.shared.animation.PhysicsAnimator; import java.util.ArrayList; @@ -40,22 +40,26 @@ import java.util.ArrayList; * Manages bubble bar expanded view menu presentation and animations */ class BubbleBarMenuViewController { - private static final float MENU_INITIAL_SCALE = 0.5f; + + private static final float WIDTH_SWAP_FRACTION = 0.4F; + private static final long MENU_ANIMATION_DURATION = 600; + private final Context mContext; private final ViewGroup mRootView; + private final BubbleBarHandleView mHandleView; private @Nullable Listener mListener; private @Nullable Bubble mBubble; private @Nullable BubbleBarMenuView mMenuView; /** A transparent view used to intercept touches to collapse menu when presented */ private @Nullable View mScrimView; - private @Nullable PhysicsAnimator<BubbleBarMenuView> mMenuAnimator; - private PhysicsAnimator.SpringConfig mMenuSpringConfig; + private @Nullable ValueAnimator mMenuAnimator; + - BubbleBarMenuViewController(Context context, ViewGroup rootView) { + BubbleBarMenuViewController(Context context, BubbleBarHandleView handleView, + ViewGroup rootView) { mContext = context; mRootView = rootView; - mMenuSpringConfig = new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); + mHandleView = handleView; } /** Tells if the menu is visible or being animated */ @@ -81,20 +85,21 @@ class BubbleBarMenuViewController { if (mMenuView == null || mScrimView == null) { setupMenu(); } - cancelAnimations(); - mMenuView.setVisibility(View.VISIBLE); - mScrimView.setVisibility(View.VISIBLE); - Runnable endActions = () -> { - mMenuView.getChildAt(0).requestAccessibilityFocus(); - if (mListener != null) { - mListener.onMenuVisibilityChanged(true /* isShown */); + runOnMenuIsMeasured(() -> { + mMenuView.setVisibility(View.VISIBLE); + mScrimView.setVisibility(View.VISIBLE); + Runnable endActions = () -> { + mMenuView.getChildAt(0).requestAccessibilityFocus(); + if (mListener != null) { + mListener.onMenuVisibilityChanged(true /* isShown */); + } + }; + if (animated) { + animateTransition(true /* show */, endActions); + } else { + endActions.run(); } - }; - if (animated) { - animateTransition(true /* show */, endActions); - } else { - endActions.run(); - } + }); } /** @@ -103,18 +108,30 @@ class BubbleBarMenuViewController { */ void hideMenu(boolean animated) { if (mMenuView == null || mScrimView == null) return; - cancelAnimations(); - Runnable endActions = () -> { - mMenuView.setVisibility(View.GONE); - mScrimView.setVisibility(View.GONE); - if (mListener != null) { - mListener.onMenuVisibilityChanged(false /* isShown */); + runOnMenuIsMeasured(() -> { + Runnable endActions = () -> { + mHandleView.restoreAnimationDefaults(); + mMenuView.setVisibility(View.GONE); + mScrimView.setVisibility(View.GONE); + mHandleView.setVisibility(View.VISIBLE); + if (mListener != null) { + mListener.onMenuVisibilityChanged(false /* isShown */); + } + }; + if (animated) { + animateTransition(false /* show */, endActions); + } else { + endActions.run(); } - }; - if (animated) { - animateTransition(false /* show */, endActions); + }); + } + + private void runOnMenuIsMeasured(Runnable action) { + if (mMenuView.getWidth() == 0 || mMenuView.getHeight() == 0) { + // the menu view is not yet measured, postpone showing the animation + mMenuView.post(() -> runOnMenuIsMeasured(action)); } else { - endActions.run(); + action.run(); } } @@ -125,24 +142,63 @@ class BubbleBarMenuViewController { */ private void animateTransition(boolean show, Runnable endActions) { if (mMenuView == null) return; - mMenuAnimator = PhysicsAnimator.getInstance(mMenuView); - mMenuAnimator.setDefaultSpringConfig(mMenuSpringConfig); - mMenuAnimator - .spring(DynamicAnimation.ALPHA, show ? 1f : 0f) - .spring(DynamicAnimation.SCALE_Y, show ? 1f : MENU_INITIAL_SCALE) - .withEndActions(() -> { - mMenuAnimator = null; - endActions.run(); - }) - .start(); + float startValue = show ? 0 : 1; + if (mMenuAnimator != null && mMenuAnimator.isRunning()) { + startValue = (float) mMenuAnimator.getAnimatedValue(); + mMenuAnimator.cancel(); + } + ValueAnimator showMenuAnimation = ValueAnimator.ofFloat(startValue, show ? 1 : 0); + showMenuAnimation.setDuration(MENU_ANIMATION_DURATION); + showMenuAnimation.setInterpolator(Interpolators.EMPHASIZED); + showMenuAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mMenuAnimator = null; + endActions.run(); + } + }); + mMenuAnimator = showMenuAnimation; + setupAnimatorListener(showMenuAnimation); + showMenuAnimation.start(); } - /** Cancel running animations */ - private void cancelAnimations() { - if (mMenuAnimator != null) { - mMenuAnimator.cancel(); - mMenuAnimator = null; + /** Setup listener that orchestrates the animation. */ + private void setupAnimatorListener(ValueAnimator showMenuAnimation) { + // Getting views properties start values + int widthDiff = mMenuView.getWidth() - mHandleView.getHandleWidth(); + int handleHeight = mHandleView.getHandleHeight(); + float targetWidth = mHandleView.getHandleWidth() + widthDiff * WIDTH_SWAP_FRACTION; + float targetHeight = targetWidth * mMenuView.getTitleItemHeight() / mMenuView.getWidth(); + int menuColor; + try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{ + com.android.internal.R.attr.materialColorSurfaceBright, + })) { + menuColor = ta.getColor(0, Color.WHITE); } + // Calculating deltas + float swapScale = targetWidth / mMenuView.getWidth(); + float handleWidthDelta = targetWidth - mHandleView.getHandleWidth(); + float handleHeightDelta = targetHeight - handleHeight; + // Setting update listener that will orchestrate the animation + showMenuAnimation.addUpdateListener(animator -> { + float animationProgress = (float) animator.getAnimatedValue(); + boolean showHandle = animationProgress <= WIDTH_SWAP_FRACTION; + mHandleView.setVisibility(showHandle ? View.VISIBLE : View.GONE); + mMenuView.setVisibility(showHandle ? View.GONE : View.VISIBLE); + if (showHandle) { + float handleAnimationProgress = animationProgress / WIDTH_SWAP_FRACTION; + mHandleView.animateHandleForMenu(handleAnimationProgress, handleWidthDelta, + handleHeightDelta, menuColor); + } else { + mMenuView.setTranslationY(mHandleView.getHandlePaddingTop()); + mMenuView.setPivotY(0); + mMenuView.setPivotX((float) mMenuView.getWidth() / 2); + float menuAnimationProgress = + (animationProgress - WIDTH_SWAP_FRACTION) / (1 - WIDTH_SWAP_FRACTION); + float currentMenuScale = swapScale + (1 - swapScale) * menuAnimationProgress; + mMenuView.animateFromStartScale(currentMenuScale, menuAnimationProgress); + } + }); } /** Sets up and inflate menu views */ @@ -150,9 +206,6 @@ class BubbleBarMenuViewController { // Menu view setup mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate( R.layout.bubble_bar_menu_view, mRootView, false); - mMenuView.setAlpha(0f); - mMenuView.setPivotY(0f); - mMenuView.setScaleY(MENU_INITIAL_SCALE); mMenuView.setOnCloseListener(() -> hideMenu(true /* animated */)); if (mBubble != null) { mMenuView.updateInfo(mBubble); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt index 4abb35c2a428..193c593e2ab2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt @@ -16,8 +16,11 @@ package com.android.wm.shell.common.pip import android.app.AppOpsManager +import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager +import android.util.Pair +import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.common.ShellExecutor class PipAppOpsListener( @@ -27,10 +30,12 @@ class PipAppOpsListener( ) { private val mAppOpsManager: AppOpsManager = checkNotNull( mContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager) + private var mTopPipActivityInfoSupplier: (Context) -> Pair<ComponentName?, Int> = + PipUtils::getTopPipActivity private val mAppOpsChangedListener = AppOpsManager.OnOpChangedListener { _, packageName -> try { // Dismiss the PiP once the user disables the app ops setting for that package - val topPipActivityInfo = PipUtils.getTopPipActivity(mContext) + val topPipActivityInfo = mTopPipActivityInfoSupplier.invoke(mContext) val componentName = topPipActivityInfo.first ?: return@OnOpChangedListener val userId = topPipActivityInfo.second val appInfo = mContext.packageManager @@ -75,4 +80,9 @@ class PipAppOpsListener( /** Dismisses the PIP window. */ fun dismissPip() } + + @VisibleForTesting + fun setTopPipActivityInfoSupplier(supplier: (Context) -> Pair<ComponentName?, Int>) { + mTopPipActivityInfoSupplier = supplier + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index a472f79c98e6..601cf70b93ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -67,6 +67,7 @@ import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; +import com.android.wm.shell.desktopmode.DesktopBackNavigationTransitionHandler; import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; @@ -836,14 +837,21 @@ public abstract class WMShellModule { @Provides static Optional<DesktopImmersiveController> provideDesktopImmersiveController( Context context, + ShellInit shellInit, Transitions transitions, @DynamicOverride DesktopRepository desktopRepository, DisplayController displayController, - ShellTaskOrganizer shellTaskOrganizer) { + ShellTaskOrganizer shellTaskOrganizer, + ShellCommandHandler shellCommandHandler) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of( new DesktopImmersiveController( - transitions, desktopRepository, displayController, shellTaskOrganizer)); + shellInit, + transitions, + desktopRepository, + displayController, + shellTaskOrganizer, + shellCommandHandler)); } return Optional.empty(); } @@ -908,6 +916,16 @@ public abstract class WMShellModule { @WMSingleton @Provides + static DesktopBackNavigationTransitionHandler provideDesktopBackNavigationTransitionHandler( + @ShellMainThread ShellExecutor mainExecutor, + @ShellAnimationThread ShellExecutor animExecutor, + DisplayController displayController) { + return new DesktopBackNavigationTransitionHandler(mainExecutor, animExecutor, + displayController); + } + + @WMSingleton + @Provides static DesktopModeDragAndDropTransitionHandler provideDesktopModeDragAndDropTransitionHandler( Transitions transitions) { return new DesktopModeDragAndDropTransitionHandler(transitions); @@ -957,6 +975,7 @@ public abstract class WMShellModule { Optional<DesktopRepository> desktopRepository, Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, + Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, ShellInit shellInit) { return desktopRepository.flatMap( repository -> @@ -966,6 +985,7 @@ public abstract class WMShellModule { repository, transitions, shellTaskOrganizer, + desktopMixedTransitionHandler.get(), shellInit))); } @@ -978,6 +998,7 @@ public abstract class WMShellModule { FreeformTaskTransitionHandler freeformTaskTransitionHandler, CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler, Optional<DesktopImmersiveController> desktopImmersiveController, + DesktopBackNavigationTransitionHandler desktopBackNavigationTransitionHandler, InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler, ShellInit shellInit, @@ -994,6 +1015,7 @@ public abstract class WMShellModule { freeformTaskTransitionHandler, closeDesktopTaskTransitionHandler, desktopImmersiveController.get(), + desktopBackNavigationTransitionHandler, interactionJankMonitor, handler, shellInit, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java index 3a4764d45f2c..3cd5df3121c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java @@ -19,6 +19,7 @@ package com.android.wm.shell.dagger.pip; import android.content.Context; import android.os.Handler; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; @@ -41,6 +42,7 @@ import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipParamsChangedForwarder; @@ -169,6 +171,8 @@ public abstract class Pip1Module { PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenControllerOptional, Optional<PipPerfHintController> pipPerfHintControllerOptional, + Optional<DesktopRepository> desktopRepositoryOptional, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { @@ -176,7 +180,8 @@ public abstract class Pip1Module { syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState, pipBoundsAlgorithm, menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, - splitScreenControllerOptional, pipPerfHintControllerOptional, displayController, + splitScreenControllerOptional, pipPerfHintControllerOptional, + desktopRepositoryOptional, rootTaskDisplayAreaOrganizer, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java index 8d1b15c1e631..78e676f8cd45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java @@ -22,6 +22,7 @@ import android.os.SystemClock; import androidx.annotation.NonNull; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; @@ -214,6 +215,7 @@ public abstract class TvPipModule { PipSurfaceTransactionHelper pipSurfaceTransactionHelper, Optional<SplitScreenController> splitScreenControllerOptional, Optional<PipPerfHintController> pipPerfHintControllerOptional, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { @@ -221,8 +223,9 @@ public abstract class TvPipModule { syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder, - splitScreenControllerOptional, pipPerfHintControllerOptional, displayController, - pipUiEventLogger, shellTaskOrganizer, mainExecutor); + splitScreenControllerOptional, pipPerfHintControllerOptional, + rootTaskDisplayAreaOrganizer, displayController, pipUiEventLogger, + shellTaskOrganizer, mainExecutor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt new file mode 100644 index 000000000000..83b0f8413a28 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.animation.Animator +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.os.IBinder +import android.util.DisplayMetrics +import android.view.SurfaceControl.Transaction +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.shared.animation.MinimizeAnimator.create +import com.android.wm.shell.transition.Transitions + +/** + * The [Transitions.TransitionHandler] that handles transitions for tasks that are closing or going + * to back as part of back navigation. This handler is used only for animating transitions. + */ +class DesktopBackNavigationTransitionHandler( + private val mainExecutor: ShellExecutor, + private val animExecutor: ShellExecutor, + private val displayController: DisplayController, +) : Transitions.TransitionHandler { + + /** Shouldn't handle anything */ + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo, + ): WindowContainerTransaction? = null + + /** Animates a transition with minimizing tasks */ + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: Transaction, + finishTransaction: Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + if (!TransitionUtil.isClosingType(info.type)) return false + + val animations = mutableListOf<Animator>() + val onAnimFinish: (Animator) -> Unit = { animator -> + mainExecutor.execute { + // Animation completed + animations.remove(animator) + if (animations.isEmpty()) { + // All animations completed, finish the transition + finishCallback.onTransitionFinished(/* wct= */ null) + } + } + } + + animations += + info.changes + .filter { + it.mode == info.type && + it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM + } + .mapNotNull { createMinimizeAnimation(it, finishTransaction, onAnimFinish) } + if (animations.isEmpty()) return false + animExecutor.execute { animations.forEach(Animator::start) } + return true + } + + private fun createMinimizeAnimation( + change: TransitionInfo.Change, + finishTransaction: Transaction, + onAnimFinish: (Animator) -> Unit + ): Animator? { + val t = Transaction() + val sc = change.leash + finishTransaction.hide(sc) + val displayMetrics: DisplayMetrics? = + change.taskInfo?.let { + displayController.getDisplayContext(it.displayId)?.getResources()?.displayMetrics + } + return displayMetrics?.let { create(it, change, t, onAnimFinish) } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt index f69aa6df6a1d..1acde73e68dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt @@ -34,10 +34,13 @@ import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionHandler import com.android.wm.shell.transition.Transitions.TransitionObserver import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener +import java.io.PrintWriter /** * A controller to move tasks in/out of desktop's full immersive state where the task @@ -45,27 +48,34 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener * be transient below the status bar like in fullscreen immersive mode. */ class DesktopImmersiveController( + shellInit: ShellInit, private val transitions: Transitions, private val desktopRepository: DesktopRepository, private val displayController: DisplayController, private val shellTaskOrganizer: ShellTaskOrganizer, + private val shellCommandHandler: ShellCommandHandler, private val transactionSupplier: () -> SurfaceControl.Transaction, ) : TransitionHandler, TransitionObserver { constructor( + shellInit: ShellInit, transitions: Transitions, desktopRepository: DesktopRepository, displayController: DisplayController, shellTaskOrganizer: ShellTaskOrganizer, + shellCommandHandler: ShellCommandHandler, ) : this( + shellInit, transitions, desktopRepository, displayController, shellTaskOrganizer, + shellCommandHandler, { SurfaceControl.Transaction() } ) - private var state: TransitionState? = null + @VisibleForTesting + var state: TransitionState? = null @VisibleForTesting val pendingExternalExitTransitions = mutableListOf<ExternalPendingExit>() @@ -79,10 +89,21 @@ class DesktopImmersiveController( /** A listener to invoke on animation changes during entry/exit. */ var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null + init { + shellInit.addInitCallback({ onInit() }, this) + } + + fun onInit() { + shellCommandHandler.addDumpCallback(this::dump, this) + } + /** Starts a transition to enter full immersive state inside the desktop. */ fun moveTaskToImmersive(taskInfo: RunningTaskInfo) { if (inProgress) { - logV("Cannot start entry because transition already in progress.") + logV( + "Cannot start entry because transition(s) already in progress: %s", + getRunningTransitions() + ) return } val wct = WindowContainerTransaction().apply { @@ -100,7 +121,10 @@ class DesktopImmersiveController( fun moveTaskToNonImmersive(taskInfo: RunningTaskInfo) { if (inProgress) { - logV("Cannot start exit because transition already in progress.") + logV( + "Cannot start exit because transition(s) already in progress: %s", + getRunningTransitions() + ) return } @@ -225,14 +249,19 @@ class DesktopImmersiveController( finishCallback: Transitions.TransitionFinishCallback ): Boolean { val state = requireState() - if (transition != state.transition) return false + check(state.transition == transition) { + "Transition $transition did not match expected state=$state" + } logD("startAnimation transition=%s", transition) animateResize( targetTaskId = state.taskId, info = info, startTransaction = startTransaction, finishTransaction = finishTransaction, - finishCallback = finishCallback + finishCallback = { + finishCallback.onTransitionFinished(/* wct= */ null) + clearState() + }, ) return true } @@ -242,12 +271,18 @@ class DesktopImmersiveController( info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, - finishCallback: Transitions.TransitionFinishCallback + finishCallback: Transitions.TransitionFinishCallback, ) { logD("animateResize for task#%d", targetTaskId) - val change = info.changes.first { c -> + val change = info.changes.firstOrNull { c -> val taskInfo = c.taskInfo - return@first taskInfo != null && taskInfo.taskId == targetTaskId + return@firstOrNull taskInfo != null && taskInfo.taskId == targetTaskId + } + if (change == null) { + logD("Did not find change for task#%d to animate", targetTaskId) + startTransaction.apply() + finishCallback.onTransitionFinished(/* wct= */ null) + return } animateResizeChange(change, startTransaction, finishTransaction, finishCallback) } @@ -288,7 +323,6 @@ class DesktopImmersiveController( .apply() onTaskResizeAnimationListener?.onAnimationEnd(taskId) finishCallback.onTransitionFinished(null /* wct */) - clearState() } ) addUpdateListener { animation -> @@ -357,8 +391,17 @@ class DesktopImmersiveController( // Check if this is a direct immersive enter/exit transition. if (transition == state?.transition) { val state = requireState() - val startBounds = info.changes.first { c -> c.taskInfo?.taskId == state.taskId } - .startAbsBounds + val immersiveChange = info.changes.firstOrNull { c -> + c.taskInfo?.taskId == state.taskId + } + if (immersiveChange == null) { + logV( + "Direct move for task#%d in %s direction missing immersive change.", + state.taskId, state.direction + ) + return + } + val startBounds = immersiveChange.startAbsBounds logV("Direct move for task#%d in %s direction verified", state.taskId, state.direction) when (state.direction) { Direction.ENTER -> { @@ -446,11 +489,30 @@ class DesktopImmersiveController( private fun requireState(): TransitionState = state ?: error("Expected non-null transition state") + private fun getRunningTransitions(): List<IBinder> { + val running = mutableListOf<IBinder>() + state?.let { + running.add(it.transition) + } + pendingExternalExitTransitions.forEach { + running.add(it.transition) + } + return running + } + private fun TransitionInfo.hasTaskChange(taskId: Int): Boolean = changes.any { c -> c.taskInfo?.taskId == taskId } + private fun dump(pw: PrintWriter, prefix: String) { + val innerPrefix = "$prefix " + pw.println("${prefix}DesktopImmersiveController") + pw.println(innerPrefix + "state=" + state) + pw.println(innerPrefix + "pendingExternalExitTransitions=" + pendingExternalExitTransitions) + } + /** The state of the currently running transition. */ - private data class TransitionState( + @VisibleForTesting + data class TransitionState( val transition: IBinder, val displayId: Int, val taskId: Int, @@ -483,7 +545,8 @@ class DesktopImmersiveController( fun asExit(): Exit? = if (this is Exit) this else null } - private enum class Direction { + @VisibleForTesting + enum class Direction { ENTER, EXIT } @@ -495,9 +558,10 @@ class DesktopImmersiveController( ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } - private companion object { + companion object { private const val TAG = "DesktopImmersive" - private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L + @VisibleForTesting + const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index 01c680dc8325..2001f9743094 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -52,6 +52,7 @@ class DesktopMixedTransitionHandler( private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler, private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler, private val desktopImmersiveController: DesktopImmersiveController, + private val desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, shellInit: ShellInit, @@ -161,6 +162,14 @@ class DesktopMixedTransitionHandler( finishTransaction, finishCallback ) + is PendingMixedTransition.Minimize -> animateMinimizeTransition( + pending, + transition, + info, + startTransaction, + finishTransaction, + finishCallback + ) } } @@ -272,6 +281,42 @@ class DesktopMixedTransitionHandler( ) } + private fun animateMinimizeTransition( + pending: PendingMixedTransition.Minimize, + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: TransitionFinishCallback, + ): Boolean { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false + + val minimizeChange = findDesktopTaskChange(info, pending.minimizingTask) + if (minimizeChange == null) { + logW("Should have minimizing desktop task") + return false + } + if (pending.isLastTask) { + // Dispatch close desktop task animation to the default transition handlers. + return dispatchToLeftoverHandler( + transition, + info, + startTransaction, + finishTransaction, + finishCallback + ) + } + + // Animate minimizing desktop task transition with [DesktopBackNavigationTransitionHandler]. + return desktopBackNavigationTransitionHandler.startAnimation( + transition, + info, + startTransaction, + finishTransaction, + finishCallback, + ) + } + override fun onTransitionConsumed( transition: IBinder, aborted: Boolean, @@ -400,6 +445,14 @@ class DesktopMixedTransitionHandler( val minimizingTask: Int?, val exitingImmersiveTask: Int?, ) : PendingMixedTransition() + + /** A task is minimizing. This should be used for task going to back and some closing cases + * with back navigation. */ + data class Minimize( + override val transition: IBinder, + val minimizingTask: Int, + val isLastTask: Boolean, + ) : PendingMixedTransition() } private fun logV(msg: String, vararg arguments: Any?) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index bed484c7a532..39586e39fdd4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -594,6 +594,10 @@ class DesktopModeEventLogger { FrameworkStatsLog .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__MAXIMIZE_MENU_RESIZE_TRIGGER ), + DRAG_TO_TOP_RESIZE_TRIGGER( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__DRAG_TO_TOP_RESIZE_TRIGGER + ), } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index fda709a4a2d7..08ca55f93e3f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -102,6 +102,9 @@ class DesktopRepository ( /* Tracks last bounds of task before toggled to stable bounds. */ private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>() + /* Tracks last bounds of task before it is minimized. */ + private val boundsBeforeMinimizeByTaskId = SparseArray<Rect>() + /* Tracks last bounds of task before toggled to immersive state. */ private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>() @@ -462,6 +465,14 @@ class DesktopRepository ( fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) = boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) + /** Removes and returns the bounds saved before minimizing the given task. */ + fun removeBoundsBeforeMinimize(taskId: Int): Rect? = + boundsBeforeMinimizeByTaskId.removeReturnOld(taskId) + + /** Saves the bounds of the given task before minimizing. */ + fun saveBoundsBeforeMinimize(taskId: Int, bounds: Rect?) = + boundsBeforeMinimizeByTaskId.set(taskId, Rect(bounds)) + /** Removes and returns the bounds saved before entering immersive with the given task. */ fun removeBoundsBeforeFullImmersive(taskId: Int): Rect? = boundsBeforeFullImmersiveByTaskId.removeReturnOld(taskId) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 162879c97a16..223038f84418 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -871,11 +871,10 @@ class DesktopTasksController( return } - // TODO(b/375356605): Introduce a new ResizeTrigger for drag-to-top. desktopModeEventLogger.logTaskResizingStarted( - ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent, taskInfo, displayController + ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent, taskInfo, displayController ) - toggleDesktopTaskSize(taskInfo, ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent) + toggleDesktopTaskSize(taskInfo, ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent) } private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect { @@ -1291,7 +1290,11 @@ class DesktopTasksController( // Check if freeform task launch during recents should be handled shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task) // Check if the closing task needs to be handled - TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task) + TransitionUtil.isClosingType(request.type) -> handleTaskClosing( + task, + transition, + request.type + ) // Check if the top task shouldn't be allowed to enter desktop mode isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task) // Check if fullscreen task should be updated @@ -1621,7 +1624,7 @@ class DesktopTasksController( } /** Handle task closing by removing wallpaper activity if it's the last active task */ - private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleTaskClosing(task: RunningTaskInfo, transition: IBinder, requestType: Int): WindowContainerTransaction? { logV("handleTaskClosing") if (!isDesktopModeShowing(task.displayId)) return null @@ -1637,8 +1640,15 @@ class DesktopTasksController( if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { taskRepository.addClosingTask(task.displayId, task.taskId) desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId) + } else if (requestType == TRANSIT_CLOSE) { + // Handle closing tasks, tasks that are going to back are handled in + // [DesktopTasksTransitionObserver]. + desktopMixedTransitionHandler.addPendingMixedTransition( + DesktopMixedTransitionHandler.PendingMixedTransition.Minimize( + transition, task.taskId, taskRepository.getVisibleTaskCount(task.displayId) == 1 + ) + ) } - taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding( task.displayId, @@ -1770,9 +1780,13 @@ class DesktopTasksController( transition: IBinder, taskIdToMinimize: Int, ) { - val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) ?: return + val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) desktopTasksLimiter.ifPresent { - it.addPendingMinimizeChange(transition, taskToMinimize.displayId, taskToMinimize.taskId) + it.addPendingMinimizeChange( + transition = transition, + displayId = taskToMinimize?.displayId ?: DEFAULT_DISPLAY, + taskId = taskIdToMinimize + ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index f0e3a2bd8ffc..77af627a948a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -92,6 +92,12 @@ class DesktopTasksLimiter ( } taskToMinimize.transitionInfo = info activeTransitionTokensAndTasks[transition] = taskToMinimize + + // Save current bounds before minimizing in case we need to restore to it later. + val boundsBeforeMinimize = info.changes.find { change -> + change.taskInfo?.taskId == taskToMinimize.taskId }?.startAbsBounds + taskRepository.saveBoundsBeforeMinimize(taskToMinimize.taskId, boundsBeforeMinimize) + this@DesktopTasksLimiter.minimizeTask( taskToMinimize.displayId, taskToMinimize.taskId) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index d1534da9a078..c39c715e685c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -46,6 +46,7 @@ class DesktopTasksTransitionObserver( private val desktopRepository: DesktopRepository, private val transitions: Transitions, private val shellTaskOrganizer: ShellTaskOrganizer, + private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, shellInit: ShellInit ) : Transitions.TransitionObserver { @@ -71,7 +72,7 @@ class DesktopTasksTransitionObserver( // TODO: b/332682201 Update repository state updateWallpaperToken(info) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { - handleBackNavigation(info) + handleBackNavigation(transition, info) removeTaskIfNeeded(info) } removeWallpaperOnLastTaskClosingIfNeeded(transition, info) @@ -95,7 +96,7 @@ class DesktopTasksTransitionObserver( } } - private fun handleBackNavigation(info: TransitionInfo) { + private fun handleBackNavigation(transition: IBinder, info: TransitionInfo) { // When default back navigation happens, transition type is TO_BACK and the change is // TO_BACK. Mark the task going to back as minimized. if (info.type == TRANSIT_TO_BACK) { @@ -105,10 +106,14 @@ class DesktopTasksTransitionObserver( continue } - if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) > 0 && + val visibleTaskCount = desktopRepository.getVisibleTaskCount(taskInfo.displayId) + if (visibleTaskCount > 0 && change.mode == TRANSIT_TO_BACK && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) + desktopMixedTransitionHandler.addPendingMixedTransition( + DesktopMixedTransitionHandler.PendingMixedTransition.Minimize( + transition, taskInfo.taskId, visibleTaskCount == 1)) } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index b27c428f1693..0154d0455e50 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -45,12 +45,17 @@ public interface Pip { } /** - * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed. + * Set the callback when isInPip state is changed. * - * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()} - * when it's changed. + * @param callback The callback accepts the state of isInPip when it's changed. */ - default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {} + default void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {} + + /** + * Remove the callback when isInPip state is changed. + * @param callback The callback accepts the state of isInPip when it's changed. + */ + default void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {} /** * Called when showing Pip menu. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 4aeecbec7dfb..5276d9d6a4df 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -27,6 +27,7 @@ import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.AppCompatTaskInfo; import android.app.TaskInfo; import android.content.Context; import android.content.pm.ActivityInfo; @@ -176,12 +177,12 @@ public class PipAnimationController { public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash, Rect baseBounds, Rect startBounds, Rect endBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, float startingAngle, - @Surface.Rotation int rotationDelta) { + @Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) { if (mCurrentAnimator == null) { mCurrentAnimator = setupPipTransitionAnimator( PipTransitionAnimator.ofBounds(taskInfo, leash, startBounds, startBounds, endBounds, sourceHintRect, direction, 0 /* startingAngle */, - rotationDelta)); + rotationDelta, alwaysAnimateTaskBounds)); } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA && mCurrentAnimator.isRunning()) { // If we are still animating the fade into pip, then just move the surface and ensure @@ -197,7 +198,8 @@ public class PipAnimationController { mCurrentAnimator.cancel(); mCurrentAnimator = setupPipTransitionAnimator( PipTransitionAnimator.ofBounds(taskInfo, leash, baseBounds, startBounds, - endBounds, sourceHintRect, direction, startingAngle, rotationDelta)); + endBounds, sourceHintRect, direction, startingAngle, rotationDelta, + alwaysAnimateTaskBounds)); } return mCurrentAnimator; } @@ -585,28 +587,32 @@ public class PipAnimationController { static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash, Rect baseValue, Rect startValue, Rect endValue, Rect sourceRectHint, @PipAnimationController.TransitionDirection int direction, float startingAngle, - @Surface.Rotation int rotationDelta) { + @Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) { final boolean isOutPipDirection = isOutPipDirection(direction); final boolean isInPipDirection = isInPipDirection(direction); // Just for simplicity we'll interpolate between the source rect hint insets and empty // insets to calculate the window crop final Rect initialSourceValue; final Rect mainWindowFrame = taskInfo.topActivityMainWindowFrame; - final boolean hasNonMatchFrame = mainWindowFrame != null; + final AppCompatTaskInfo compatInfo = taskInfo.appCompatTaskInfo; + final boolean isSizeCompatOrLetterboxed = compatInfo.isTopActivityInSizeCompat() + || compatInfo.isTopActivityLetterboxed(); + // For the animation to swipe PIP to home or restore a PIP task from home, we don't + // override to the main window frame since we should animate the whole task. + final boolean shouldUseMainWindowFrame = mainWindowFrame != null + && !alwaysAnimateTaskBounds && !isSizeCompatOrLetterboxed; final boolean changeOrientation = rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270; final Rect baseBounds = new Rect(baseValue); final Rect startBounds = new Rect(startValue); final Rect endBounds = new Rect(endValue); if (isOutPipDirection) { - // TODO(b/356277166): handle rotation change with activity that provides main window - // frame. - if (hasNonMatchFrame && !changeOrientation) { + if (shouldUseMainWindowFrame && !changeOrientation) { endBounds.set(mainWindowFrame); } initialSourceValue = new Rect(endBounds); } else if (isInPipDirection) { - if (hasNonMatchFrame) { + if (shouldUseMainWindowFrame) { baseBounds.set(mainWindowFrame); if (startValue.equals(baseValue)) { // If the start value is at initial state as in PIP animation, also override @@ -635,9 +641,19 @@ public class PipAnimationController { if (changeOrientation) { lastEndRect = new Rect(endBounds); rotatedEndRect = new Rect(endBounds); - // Rotate the end bounds according to the rotation delta because the display will - // be rotated to the same orientation. - rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta); + // TODO(b/375977163): polish the animation to restoring the PIP task back from + // swipe-pip-to-home. Ideally we should send the transitionInfo after reparenting + // the PIP activity back to the original task. + if (shouldUseMainWindowFrame) { + // If we should animate the main window frame, set it to the rotatedRect + // instead. The end bounds reported by transitionInfo is the bounds before + // rotation, while main window frame is calculated after the rotation. + rotatedEndRect.set(mainWindowFrame); + } else { + // Rotate the end bounds according to the rotation delta because the display + // will be rotated to the same orientation. + rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta); + } // Use the rect that has the same orientation as the hint rect. initialContainerRect = isOutPipDirection ? rotatedEndRect : initialSourceValue; } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index c4e63dfdade9..30f1948efa2d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -67,6 +68,7 @@ import android.view.Choreographer; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; +import android.window.DisplayAreaInfo; import android.window.TaskOrganizer; import android.window.TaskSnapshot; import android.window.WindowContainerToken; @@ -74,7 +76,9 @@ import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; +import com.android.window.flags.Flags; import com.android.wm.shell.R; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ScreenshotUtils; @@ -87,6 +91,7 @@ import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.Interpolators; @@ -145,6 +150,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final Optional<SplitScreenController> mSplitScreenOptional; @Nullable private final PipPerfHintController mPipPerfHintController; + private final Optional<DesktopRepository> mDesktopRepositoryOptional; + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; protected final ShellTaskOrganizer mTaskOrganizer; protected final ShellExecutor mMainExecutor; @@ -388,6 +395,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenOptional, Optional<PipPerfHintController> pipPerfHintControllerOptional, + Optional<DesktopRepository> desktopRepositoryOptional, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @@ -414,6 +423,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); mSplitScreenOptional = splitScreenOptional; mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); + mDesktopRepositoryOptional = desktopRepositoryOptional; + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mTaskOrganizer = shellTaskOrganizer; mMainExecutor = mainExecutor; @@ -741,10 +752,23 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** Returns the bounds to restore to when exiting PIP mode. */ + // TODO(b/377581840): Instead of manually tracking bounds, use bounds from Core. public Rect getExitDestinationBounds() { + if (isPipLaunchedInDesktopMode()) { + final Rect freeformBounds = mDesktopRepositoryOptional.get().removeBoundsBeforeMinimize( + mTaskInfo.taskId); + return Objects.requireNonNullElseGet(freeformBounds, mPipBoundsState::getDisplayBounds); + } return mPipBoundsState.getDisplayBounds(); } + /** Returns whether PiP was launched while in desktop mode. */ + // TODO(377581840): Update this check to include non-minimized cases, e.g. split to PiP etc. + private boolean isPipLaunchedInDesktopMode() { + return Flags.enableDesktopWindowingPip() && mDesktopRepositoryOptional.isPresent() + && mDesktopRepositoryOptional.get().isMinimizedTask(mTaskInfo.taskId); + } + private void exitLaunchIntoPipTask(WindowContainerTransaction wct) { wct.startTask(mTaskInfo.launchIntoPipHostTaskId, null /* ActivityOptions */); mTaskOrganizer.applyTransaction(wct); @@ -1808,7 +1832,25 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * and can be overridden to restore to an alternate windowing mode. */ public int getOutPipWindowingMode() { - // By default, simply reset the windowing mode to undefined. + final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( + mTaskInfo.displayId); + + // If PiP was launched while in desktop mode (we should return the task to freeform + // windowing mode): + // 1) If the display windowing mode is freeform, set windowing mode to undefined so it will + // resolve the windowing mode to the display's windowing mode. + // 2) If the display windowing mode is not freeform, set windowing mode to freeform. + if (tdaInfo != null && isPipLaunchedInDesktopMode()) { + final int displayWindowingMode = + tdaInfo.configuration.windowConfiguration.getWindowingMode(); + if (displayWindowingMode == WINDOWING_MODE_FREEFORM) { + return WINDOWING_MODE_UNDEFINED; + } else { + return WINDOWING_MODE_FREEFORM; + } + } + + // By default, or if the task is going to fullscreen, reset the windowing mode to undefined. return WINDOWING_MODE_UNDEFINED; } @@ -1838,9 +1880,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, ? mPipBoundsState.getBounds() : currentBounds; final boolean existingAnimatorRunning = mPipAnimationController.getCurrentAnimator() != null && mPipAnimationController.getCurrentAnimator().isRunning(); + // For resize animation, we always animate the whole PIP task bounds. final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds, - sourceHintRect, direction, startingAngle, rotationDelta); + sourceHintRect, direction, startingAngle, rotationDelta, + true /* alwaysAnimateTaskBounds */); animator.setTransitionDirection(direction) .setPipTransactionHandler(mPipTransactionHandler) .setDuration(durationMs); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 28b91c6cb812..f7aed4401247 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -530,6 +530,13 @@ public class PipTransition extends PipTransitionController { if (mFixedRotationState != FIXED_ROTATION_TRANSITION && mFinishTransaction != null) { mFinishTransaction.merge(tx); + // Set window crop and position to destination bounds to avoid flickering. + if (hasValidLeash) { + mFinishTransaction.setWindowCrop(leash, destinationBounds.width(), + destinationBounds.height()); + mFinishTransaction.setPosition(leash, destinationBounds.left, + destinationBounds.top); + } } } else { wct = new WindowContainerTransaction(); @@ -884,7 +891,8 @@ public class PipTransition extends PipTransitionController { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(taskInfo, pipChange.getLeash(), startBounds, startBounds, endBounds, null, TRANSITION_DIRECTION_LEAVE_PIP, - 0 /* startingAngle */, pipRotateDelta); + 0 /* startingAngle */, pipRotateDelta, + false /* alwaysAnimateTaskBounds */); animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) @@ -899,7 +907,7 @@ public class PipTransition extends PipTransitionController { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(taskInfo, leash, baseBounds, startBounds, endBounds, sourceHintRect, TRANSITION_DIRECTION_LEAVE_PIP, - 0 /* startingAngle */, rotationDelta); + 0 /* startingAngle */, rotationDelta, false /* alwaysAnimateTaskBounds */); animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) .setDuration(mEnterExitAnimationDuration); if (startTransaction != null) { @@ -1095,8 +1103,6 @@ public class PipTransition extends PipTransitionController { if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() && mPipTransitionState.getInSwipePipToHomeTransition()) { - // TODO(b/356277166): add support to swipe PIP to home with - // non-match parent activity. handleSwipePipToHomeTransition(startTransaction, finishTransaction, leash, sourceHintRect, destinationBounds, taskInfo); return; @@ -1118,7 +1124,7 @@ public class PipTransition extends PipTransitionController { if (enterAnimationType == ANIM_TYPE_BOUNDS) { animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds, currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, - 0 /* startingAngle */, rotationDelta); + 0 /* startingAngle */, rotationDelta, false /* alwaysAnimateTaskBounds */); if (sourceHintRect == null) { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. We also temporarily disallow app icon overlay and use color overlay @@ -1241,10 +1247,14 @@ public class PipTransition extends PipTransitionController { // to avoid flicker. final Rect savedDisplayCutoutInsets = new Rect(pipTaskInfo.displayCutoutInsets); pipTaskInfo.displayCutoutInsets.setEmpty(); + // Always use the task bounds even if the PIP activity doesn't match parent because the app + // and the whole task will move behind. We should animate the whole task bounds in this + // case. final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, - 0 /* startingAngle */, ROTATION_0 /* rotationDelta */) + 0 /* startingAngle */, ROTATION_0 /* rotationDelta */, + true /* alwaysAnimateTaskBounds */) .setPipTransactionHandler(mTransactionConsumer) .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP); // The start state is the end state for swipe-auto-pip. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 7f6118689dad..588b88753eb9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -104,6 +104,7 @@ import com.android.wm.shell.sysui.UserChangeListener; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -215,7 +216,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private boolean mIsKeyguardShowingOrAnimating; - private Consumer<Boolean> mOnIsInPipStateChangedListener; + private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>(); @VisibleForTesting interface PipAnimationListener { @@ -501,11 +502,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb false /* saveRestoreSnapFraction */); }); mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> { - if (mOnIsInPipStateChangedListener != null) { - final boolean wasInPip = PipTransitionState.isInPip(oldState); - final boolean nowInPip = PipTransitionState.isInPip(newState); - if (nowInPip != wasInPip) { - mOnIsInPipStateChangedListener.accept(nowInPip); + final boolean wasInPip = PipTransitionState.isInPip(oldState); + final boolean nowInPip = PipTransitionState.isInPip(newState); + if (nowInPip != wasInPip) { + for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) { + listener.accept(nowInPip); } } }); @@ -960,13 +961,19 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx); } - private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { - mOnIsInPipStateChangedListener = callback; - if (mOnIsInPipStateChangedListener != null) { + private void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + if (callback != null) { + mOnIsInPipStateChangedListeners.add(callback); callback.accept(mPipTransitionState.isInPip()); } } + private void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + if (callback != null) { + mOnIsInPipStateChangedListeners.remove(callback); + } + } + private void setShelfHeightLocked(boolean visible, int height) { final int shelfHeight = visible ? height : 0; mPipBoundsState.setShelfVisibility(visible, shelfHeight); @@ -1222,9 +1229,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + public void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + mMainExecutor.execute(() -> { + PipController.this.addOnIsInPipStateChangedListener(callback); + }); + } + + @Override + public void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { mMainExecutor.execute(() -> { - PipController.this.setOnIsInPipStateChangedListener(callback); + PipController.this.removeOnIsInPipStateChangedListener(callback); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java index 614ef2ab9831..fcba46108f67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -21,6 +21,7 @@ import android.content.Context; import androidx.annotation.NonNull; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; @@ -61,6 +62,7 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenOptional, Optional<PipPerfHintController> pipPerfHintControllerOptional, + @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @@ -68,8 +70,9 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController, surfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder, - splitScreenOptional, pipPerfHintControllerOptional, displayController, - pipUiEventLogger, shellTaskOrganizer, mainExecutor); + splitScreenOptional, pipPerfHintControllerOptional, Optional.empty(), + rootTaskDisplayAreaOrganizer, displayController, pipUiEventLogger, + shellTaskOrganizer, mainExecutor); mTvPipTransition = tvPipTransition; } 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 index d3f537b8f904..bc0918331168 100644 --- 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 @@ -21,6 +21,7 @@ import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.ComponentName; @@ -66,6 +67,8 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; /** @@ -94,7 +97,7 @@ public class PipController implements ConfigurationChangeListener, private final PipTouchHandler mPipTouchHandler; private final ShellExecutor mMainExecutor; private final PipImpl mImpl; - private Consumer<Boolean> mOnIsInPipStateChangedListener; + private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>(); // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents. private PipAnimationListener mPipRecentsAnimationListener; @@ -413,13 +416,13 @@ public class PipController implements ConfigurationChangeListener, if (mPipTransitionState.isInSwipePipToHomeTransition()) { mPipTransitionState.resetSwipePipToHomeState(); } - if (mOnIsInPipStateChangedListener != null) { - mOnIsInPipStateChangedListener.accept(true /* inPip */); + for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) { + listener.accept(true /* inPip */); } break; case PipTransitionState.EXITED_PIP: - if (mOnIsInPipStateChangedListener != null) { - mOnIsInPipStateChangedListener.accept(false /* inPip */); + for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) { + listener.accept(false /* inPip */); } break; } @@ -451,13 +454,19 @@ public class PipController implements ConfigurationChangeListener, mPipTransitionState.dump(pw, innerPrefix); } - private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { - mOnIsInPipStateChangedListener = callback; - if (mOnIsInPipStateChangedListener != null) { + private void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + if (callback != null) { + mOnIsInPipStateChangedListeners.add(callback); callback.accept(mPipTransitionState.isInPip()); } } + private void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + if (callback != null) { + mOnIsInPipStateChangedListeners.remove(callback); + } + } + private void setLauncherAppIconSize(int iconSizePx) { mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx); } @@ -473,9 +482,16 @@ public class PipController implements ConfigurationChangeListener, public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {} @Override - public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + public void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + mMainExecutor.execute(() -> { + PipController.this.addOnIsInPipStateChangedListener(callback); + }); + } + + @Override + public void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { mMainExecutor.execute(() -> { - PipController.this.setOnIsInPipStateChangedListener(callback); + PipController.this.removeOnIsInPipStateChangedListener(callback); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index ea783e9cadb6..3caad0966b1f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; @@ -230,6 +231,11 @@ public class PipTransition extends PipTransitionController implements // If there is no PiP change, exit this transition handler and potentially try others. if (pipChange == null) return false; + // Other targets might have default transforms applied that are not relevant when + // playing PiP transitions, so reset those transforms if needed. + prepareOtherTargetTransforms(info, startTransaction, finishTransaction); + + // Update the PipTransitionState while supplying the PiP leash and token to be cached. Bundle extra = new Bundle(); extra.putParcelable(PIP_TASK_TOKEN, pipChange.getContainer()); extra.putParcelable(PIP_TASK_LEASH, pipChange.getLeash()); @@ -341,17 +347,21 @@ public class PipTransition extends PipTransitionController implements (destinationBounds.height() - overlaySize) / 2f); } - final int startRotation = pipChange.getStartRotation(); - final int endRotation = mPipDisplayLayoutState.getRotation(); - final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : startRotation - endRotation; + final int delta = getFixedRotationDelta(info, pipChange); if (delta != ROTATION_0) { - mPipTransitionState.setInFixedRotation(true); - handleBoundsEnterFixedRotation(pipChange, pipActivityChange, endRotation); + // Update transition target changes in place to prepare for fixed rotation. + handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); } + // Update the src-rect-hint in params in place, to set up initial animator transform. + Rect sourceRectHint = getAdjustedSourceRectHint(info, pipChange, pipActivityChange); + pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint().set(sourceRectHint); + + // Config-at-end transitions need to have their activities transformed before starting + // the animation; this makes the buffer seem like it's been updated to final size. prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange, pipActivityChange); + startTransaction.merge(finishTransaction); PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash, startTransaction, finishTransaction, destinationBounds, delta); @@ -387,55 +397,36 @@ public class PipTransition extends PipTransitionController implements return false; } + final SurfaceControl pipLeash = getLeash(pipChange); final Rect startBounds = pipChange.getStartAbsBounds(); final Rect endBounds = pipChange.getEndAbsBounds(); - final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; - final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params); - final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, - endBounds); - final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) - : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio); - - final SurfaceControl pipLeash = mPipTransitionState.getPinnedTaskLeash(); - - // For opening type transitions, if there is a change of mode TO_FRONT/OPEN, - // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f - // by the Transitions framework to simplify Task opening transitions. - if (TransitionUtil.isOpeningType(info.getType())) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getLeash() == null) continue; - if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { - startTransaction.setAlpha(change.getLeash(), 1f); - } - } - } - - final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); - final int startRotation = pipChange.getStartRotation(); - final int endRotation = fixedRotationChange != null - ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED; - final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : startRotation - endRotation; + final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange, + pipActivityChange); + final int delta = getFixedRotationDelta(info, pipChange); if (delta != ROTATION_0) { - mPipTransitionState.setInFixedRotation(true); - handleBoundsEnterFixedRotation(pipChange, pipActivityChange, - fixedRotationChange.getEndFixedRotation()); + // Update transition target changes in place to prepare for fixed rotation. + handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); } PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash, startTransaction, finishTransaction, endBounds, delta); - if (sourceRectHint == null) { - // update the src-rect-hint in params in place, to set up initial animator transform. - params.getSourceRectHint().set(adjustedSourceRectHint); + if (PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, endBounds) == null) { + // If app provided src-rect-hint is invalid, use app icon overlay. animator.setAppIconContentOverlay( mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); } + // Update the src-rect-hint in params in place, to set up initial animator transform. + params.getSourceRectHint().set(adjustedSourceRectHint); + + // Config-at-end transitions need to have their activities transformed before starting + // the animation; this makes the buffer seem like it's been updated to final size. prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange, pipActivityChange); + animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange)); animator.setAnimationEndCallback(() -> { if (animator.getContentOverlayLeash() != null) { @@ -457,11 +448,22 @@ public class PipTransition extends PipTransitionController implements animator.start(); } - private void handleBoundsEnterFixedRotation(TransitionInfo.Change pipTaskChange, - TransitionInfo.Change pipActivityChange, int endRotation) { - final Rect endBounds = pipTaskChange.getEndAbsBounds(); - final Rect endActivityBounds = pipActivityChange.getEndAbsBounds(); - int startRotation = pipTaskChange.getStartRotation(); + private void handleBoundsEnterFixedRotation(TransitionInfo info, + TransitionInfo.Change outPipTaskChange, + TransitionInfo.Change outPipActivityChange) { + final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + final Rect endBounds = outPipTaskChange.getEndAbsBounds(); + final Rect endActivityBounds = outPipActivityChange.getEndAbsBounds(); + int startRotation = outPipTaskChange.getStartRotation(); + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation(); + + if (startRotation == endRotation) { + return; + } + + // This is used by display change listeners to respond properly to fixed rotation. + mPipTransitionState.setInFixedRotation(true); // Cache the task to activity offset to potentially restore later. Point activityEndOffset = new Point(endActivityBounds.left - endBounds.left, @@ -490,15 +492,15 @@ public class PipTransition extends PipTransitionController implements endBounds.top + activityEndOffset.y); } - private void handleExpandFixedRotation(TransitionInfo.Change pipTaskChange, int endRotation) { - final Rect endBounds = pipTaskChange.getEndAbsBounds(); + private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) { + final Rect endBounds = outPipTaskChange.getEndAbsBounds(); final int width = endBounds.width(); final int height = endBounds.height(); final int left = endBounds.left; final int top = endBounds.top; int newTop, newLeft; - if (endRotation == Surface.ROTATION_90) { + if (delta == Surface.ROTATION_90) { newLeft = top; newTop = -(left + width); } else { @@ -585,15 +587,11 @@ public class PipTransition extends PipTransitionController implements final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds, startBounds); - final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); - final int startRotation = pipChange.getStartRotation(); - final int endRotation = fixedRotationChange != null - ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED; - final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : endRotation - startRotation; - + // We define delta = startRotation - endRotation, so we need to flip the sign. + final int delta = -getFixedRotationDelta(info, pipChange); if (delta != ROTATION_0) { - handleExpandFixedRotation(pipChange, endRotation); + // Update PiP target change in place to prepare for fixed rotation; + handleExpandFixedRotation(pipChange, delta); } PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash, @@ -661,6 +659,72 @@ public class PipTransition extends PipTransitionController implements return null; } + @NonNull + private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change pipTaskChange, + @NonNull TransitionInfo.Change pipActivityChange) { + final Rect startBounds = pipTaskChange.getStartAbsBounds(); + final Rect endBounds = pipTaskChange.getEndAbsBounds(); + final PictureInPictureParams params = pipTaskChange.getTaskInfo().pictureInPictureParams; + + // Get the source-rect-hint provided by the app and check its validity; null if invalid. + final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, + endBounds); + + final Rect adjustedSourceRectHint = new Rect(); + if (sourceRectHint != null) { + adjustedSourceRectHint.set(sourceRectHint); + // If multi-activity PiP, use the parent task before PiP to retrieve display cutouts; + // then, offset the valid app provided source rect hint by the cutout insets. + // For single-activity PiP, just use the pinned task to get the cutouts instead. + TransitionInfo.Change parentBeforePip = pipActivityChange.getLastParent() != null + ? getChangeByToken(info, pipActivityChange.getLastParent()) : null; + Rect cutoutInsets = parentBeforePip != null + ? parentBeforePip.getTaskInfo().displayCutoutInsets + : pipTaskChange.getTaskInfo().displayCutoutInsets; + if (cutoutInsets != null + && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) { + adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top); + } + } else { + // For non-valid app provided src-rect-hint, calculate one to crop into during + // app icon overlay animation. + float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params); + adjustedSourceRectHint.set( + PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio)); + } + return adjustedSourceRectHint; + } + + @Surface.Rotation + private int getFixedRotationDelta(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change pipChange) { + TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + int startRotation = pipChange.getStartRotation(); + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation(); + int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 + : startRotation - endRotation; + return delta; + } + + private void prepareOtherTargetTransforms(TransitionInfo info, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction) { + // For opening type transitions, if there is a change of mode TO_FRONT/OPEN, + // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f + // by the Transitions framework to simplify Task opening transitions. + if (TransitionUtil.isOpeningType(info.getType())) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getLeash() == null) continue; + if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { + startTransaction.setAlpha(change.getLeash(), 1f); + } + } + } + + } + private WindowContainerTransaction getEnterPipTransaction(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { // cache the original task token to check for multi-activity case later 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 40065b9287a6..9016c45e8197 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 @@ -34,6 +34,8 @@ import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; +import static com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION; +import static com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -67,6 +69,7 @@ import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; @@ -216,8 +219,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, break; } } - final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct, - mixer == null ? this : mixer); + final int transitionType = Flags.enableShellTopTaskTracking() + ? TRANSIT_START_RECENTS_TRANSITION + : TRANSIT_TO_FRONT; + final IBinder transition = mTransitions.startTransition(transitionType, + wct, mixer == null ? this : mixer); if (mixer != null) { setTransitionForMixer.accept(transition); } @@ -300,7 +306,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } - controller.merge(info, t, finishCallback); + controller.merge(info, t, mergeTarget, finishCallback); } @Override @@ -367,6 +373,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, private boolean mPausingSeparateHome = false; private ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = null; private PictureInPictureSurfaceTransaction mPipTransaction = null; + // This is the transition that backs the entire recents transition, and the one that the + // pending finish transition below will be merged into private IBinder mTransition = null; private boolean mKeyguardLocked = false; private boolean mWillFinishToHome = false; @@ -386,6 +394,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // next called. private Pair<int[], TaskSnapshot[]> mPendingPauseSnapshotsForCancel; + // Used to track a pending finish transition + private IBinder mPendingFinishTransition; + private IResultReceiver mPendingRunnerFinishCb; + RecentsController(IRecentsAnimationRunner listener) { mInstanceId = System.identityHashCode(this); mListener = listener; @@ -523,6 +535,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mInfo = null; mTransition = null; mPendingPauseSnapshotsForCancel = null; + mPipTaskId = -1; + mPipTask = null; + mPipTransaction = null; + mPendingRunnerFinishCb = null; + mPendingFinishTransition = null; mControllers.remove(this); for (int i = 0; i < mStateListeners.size(); i++) { mStateListeners.get(i).onAnimationStateChanged(false); @@ -734,6 +751,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // the pausing apps. t.setLayer(target.leash, layer); } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " not handling home taskId=%d", taskInfo.taskId); // do nothing } else if (TransitionUtil.isOpeningType(change.getMode())) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -872,16 +891,35 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } } + /** + * Note: because we use a book-end transition to finish the recents transition, we must + * either always merge the incoming transition, or always cancel the recents transition + * if we don't handle the incoming transition to ensure that the end transition is queued + * before any unhandled transitions. + */ @SuppressLint("NewApi") - void merge(TransitionInfo info, SurfaceControl.Transaction t, + void merge(TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { if (mFinishCB == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: skip, no finish callback", mInstanceId); - // This was no-op'd (likely a repeated start) and we've already sent finish. + // This was no-op'd (likely a repeated start) and we've already completed finish. + return; + } + + if (Flags.enableShellTopTaskTracking() + && info.getType() == TRANSIT_END_RECENTS_TRANSITION + && mergeTarget == mTransition) { + // This is a pending finish, so merge the end transition to trigger completing the + // cleanup of the recents transition + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION", + mInstanceId); + finishCallback.onTransitionFinished(null /* wct */); return; } + if (info.getType() == TRANSIT_SLEEP) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: transit_sleep", mInstanceId); @@ -1245,7 +1283,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, return; } - if (mFinishCB == null) { + if (mFinishCB == null + || (Flags.enableShellTopTaskTracking() && mPendingFinishTransition != null)) { Slog.e(TAG, "Duplicate call to finish"); return; } @@ -1254,19 +1293,22 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, && !mWillFinishToHome && mPausingTasks != null && mState == STATE_NORMAL; - if (returningToApp && allAppsAreTranslucent(mPausingTasks)) { - mHomeTransitionObserver.notifyHomeVisibilityChanged(true); - } else if (!toHome) { - // For some transitions, we may have notified home activity that it became visible. - // We need to notify the observer that we are no longer going home. - mHomeTransitionObserver.notifyHomeVisibilityChanged(false); + if (!Flags.enableShellTopTaskTracking()) { + // This is only necessary when the recents transition is finished using a finishWCT, + // otherwise a new transition will notify the relevant observers + if (returningToApp && allAppsAreTranslucent(mPausingTasks)) { + mHomeTransitionObserver.notifyHomeVisibilityChanged(true); + } else if (!toHome) { + // For some transitions, we may have notified home activity that it became + // visible. We need to notify the observer that we are no longer going home. + mHomeTransitionObserver.notifyHomeVisibilityChanged(false); + } } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.finishInner: toHome=%b userLeave=%b " - + "willFinishToHome=%b state=%d", - mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState); - final Transitions.TransitionFinishCallback finishCB = mFinishCB; - mFinishCB = null; + + "willFinishToHome=%b state=%d reason=%s", + mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState, reason); final SurfaceControl.Transaction t = mFinishTransaction; final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -1328,6 +1370,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, for (int i = 0; i < mClosingTasks.size(); ++i) { cleanUpPausingOrClosingTask(mClosingTasks.get(i), wct, t, sendUserLeaveHint); } + if (mPipTransaction != null && sendUserLeaveHint) { SurfaceControl pipLeash = null; TransitionInfo.Change pipChange = null; @@ -1379,15 +1422,50 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mTransitions.startTransition(TRANSIT_PIP, wct, null /* handler */); // We need to clear the WCT to send finishWCT=null for Recents. wct.clear(); + + if (Flags.enableShellTopTaskTracking()) { + // In this case, we've already started the PIP transition, so we can + // clean up immediately + mPendingRunnerFinishCb = runnerFinishCb; + onFinishInner(null); + return; + } } } - mPipTaskId = -1; - mPipTask = null; - mPipTransaction = null; } } + + if (Flags.enableShellTopTaskTracking()) { + if (!wct.isEmpty()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.finishInner: " + + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId); + mPendingRunnerFinishCb = runnerFinishCb; + mPendingFinishTransition = mTransitions.startTransition( + TRANSIT_END_RECENTS_TRANSITION, wct, + new PendingFinishTransitionHandler()); + } else { + // If there's no work to do, just go ahead and clean up + mPendingRunnerFinishCb = runnerFinishCb; + onFinishInner(null /* wct */); + } + } else { + mPendingRunnerFinishCb = runnerFinishCb; + onFinishInner(wct); + } + } + + /** + * Runs the actual logic to finish the recents transition. + */ + private void onFinishInner(@Nullable WindowContainerTransaction wct) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.finishInner: Completing finish", mInstanceId); + final Transitions.TransitionFinishCallback finishCb = mFinishCB; + final IResultReceiver runnerFinishCb = mPendingRunnerFinishCb; + cleanUp(); - finishCB.onTransitionFinished(wct.isEmpty() ? null : wct); + finishCb.onTransitionFinished(wct); if (runnerFinishCb != null) { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -1472,6 +1550,40 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } }); } + + /** + * A temporary transition handler used with the pending finish transition, which runs the + * cleanup/finish logic once the pending transition is merged/handled. + * This is only initialized if Flags.enableShellTopTaskTracking() is enabled. + */ + private class PendingFinishTransitionHandler implements Transitions.TransitionHandler { + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishTransaction) { + // Once we have merged (or not if the WCT didn't result in any changes), then we can + // run the pending finish logic + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.onTransitionConsumed: " + + "Consumed pending finish transition", mInstanceId); + onFinishInner(null /* wct */); + } + }; }; /** Utility class to track the state of a task as-seen by recents. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 29e4b5bca5cc..9fcf98b9efc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -353,7 +353,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { boolean isSeamlessDisplayChange = false; if (mode == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) { - if (info.getType() == TRANSIT_CHANGE) { + if (info.getType() == TRANSIT_CHANGE || isOnlyTranslucent) { final int anim = getRotationAnimationHint(change, info, mDisplayController); isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 1d456aed5f4d..3f191497e1ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -203,6 +203,12 @@ public class Transitions implements RemoteCallable<Transitions>, /** Transition type to minimize a task. */ public static final int TRANSIT_MINIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 20; + /** Transition to start the recents transition */ + public static final int TRANSIT_START_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 21; + + /** Transition to end the recents transition */ + public static final int TRANSIT_END_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 22; + /** Transition type for desktop mode transitions. */ public static final int TRANSIT_DESKTOP_MODE_TYPES = WindowManager.TRANSIT_FIRST_CUSTOM + 100; @@ -1875,6 +1881,8 @@ public class Transitions implements RemoteCallable<Transitions>, case TRANSIT_SPLIT_PASSTHROUGH -> "SPLIT_PASSTHROUGH"; case TRANSIT_CLEANUP_PIP_EXIT -> "CLEANUP_PIP_EXIT"; case TRANSIT_MINIMIZE -> "MINIMIZE"; + case TRANSIT_START_RECENTS_TRANSITION -> "START_RECENTS_TRANSITION"; + case TRANSIT_END_RECENTS_TRANSITION -> "END_RECENTS_TRANSITION"; default -> ""; }; return typeStr + "(FIRST_CUSTOM+" + (transitType - TRANSIT_FIRST_CUSTOM) + ")"; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 7265fb8f8027..c9f2d2e8c0e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -26,6 +26,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; +import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.content.ContentResolver; import android.content.Context; @@ -110,6 +111,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT } mMainExecutor.execute(() -> { mExclusionRegion.set(systemGestureExclusion); + onExclusionRegionChanged(displayId, mExclusionRegion); }); } }; @@ -163,7 +165,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT boolean isFocusedGlobally) { final WindowDecoration decor = mWindowDecorByTaskId.get(taskId); if (decor != null) { - decor.relayout(decor.mTaskInfo, isFocusedGlobally); + decor.relayout(decor.mTaskInfo, isFocusedGlobally, decor.mExclusionRegion); } } @@ -199,9 +201,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT if (enableDisplayFocusInShellTransitions()) { // Pass the current global focus status to avoid updates outside of a ShellTransition. - decoration.relayout(taskInfo, decoration.mHasGlobalFocus); + decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion); } else { - decoration.relayout(taskInfo, taskInfo.isFocused); + decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion); } } @@ -240,7 +242,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT } else { decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion); } } @@ -254,7 +256,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion); } @Override @@ -266,6 +268,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT decoration.close(); } + private void onExclusionRegionChanged(int displayId, @NonNull Region exclusionRegion) { + final int decorCount = mWindowDecorByTaskId.size(); + for (int i = 0; i < decorCount; i++) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i); + if (decoration.mTaskInfo.displayId != displayId) continue; + decoration.onExclusionRegionChanged(exclusionRegion); + } + } + private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { return true; @@ -333,7 +344,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT windowDecoration.setTaskDragResizer(taskPositioner); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion); } private class CaptionTouchEventListener implements diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index c9546731a193..982fda0ddf36 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -36,6 +36,7 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; import android.graphics.drawable.GradientDrawable; import android.os.Handler; import android.util.Size; @@ -174,7 +175,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } @Override - void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus) { + void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); // The crop and position of the task should only be set when a task is fluid resizing. In // all other cases, it is expected that the transition handler positions and crops the task @@ -186,7 +188,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL // synced with the buffer transaction (that draws the View). Both will be shown on screen // at the same, whereas applying them independently causes flickering. See b/270202228. relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */, - shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); } @VisibleForTesting @@ -198,7 +200,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, InsetsState displayInsetsState, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, + @NonNull Region globalExclusionRegion) { relayoutParams.reset(); relayoutParams.mRunningTaskInfo = taskInfo; relayoutParams.mLayoutResId = R.layout.caption_window_decor; @@ -210,6 +213,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop; relayoutParams.mIsCaptionVisible = taskInfo.isFreeform() || (isStatusBarVisible && !isKeyguardVisibleAndOccluded); + relayoutParams.mDisplayExclusionRegion.set(globalExclusionRegion); if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { // If the app is requesting to customize the caption bar, allow input to fall @@ -236,7 +240,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, + @NonNull Region globalExclusionRegion) { final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; final boolean isDragResizeable = ENABLE_WINDOWING_SCALED_RESIZING.isTrue() @@ -249,7 +254,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, - mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus); + mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, + globalExclusionRegion); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index f2d8a782de34..a2d81a0ed90a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -220,6 +220,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } mMainExecutor.execute(() -> { mExclusionRegion.set(systemGestureExclusion); + onExclusionRegionChanged(displayId, mExclusionRegion); }); } }; @@ -432,7 +433,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, boolean isFocusedGlobally) { final WindowDecoration decor = mWindowDecorByTaskId.get(taskId); if (decor != null) { - decor.relayout(decor.mTaskInfo, isFocusedGlobally); + decor.relayout(decor.mTaskInfo, isFocusedGlobally, decor.mExclusionRegion); } } @@ -465,15 +466,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo; if (taskInfo.displayId != oldTaskInfo.displayId - && !Flags.enableHandleInputFix()) { + && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { removeTaskFromEventReceiver(oldTaskInfo.displayId); incrementEventReceiverTasks(taskInfo.displayId); } if (enableDisplayFocusInShellTransitions()) { // Pass the current global focus status to avoid updates outside of a ShellTransition. - decoration.relayout(taskInfo, decoration.mHasGlobalFocus); + decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion); } else { - decoration.relayout(taskInfo, taskInfo.isFocused); + decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion); } mActivityOrientationChangeHandler.ifPresent(handler -> handler.handleActivityOrientationChange(oldTaskInfo, taskInfo)); @@ -514,7 +515,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } else { decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), + mExclusionRegion); } } @@ -528,7 +530,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); + mFocusTransitionObserver.hasGlobalFocus(taskInfo), + mExclusionRegion); } @Override @@ -539,7 +542,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.close(); final int displayId = taskInfo.displayId; if (mEventReceiversByDisplay.contains(displayId) - && !Flags.enableHandleInputFix()) { + && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { removeTaskFromEventReceiver(displayId); } // Remove the decoration from the cache last because WindowDecoration#close could still @@ -548,6 +551,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mWindowDecorByTaskId.remove(taskInfo.taskId); } + private void onExclusionRegionChanged(int displayId, @NonNull Region exclusionRegion) { + final int decorCount = mWindowDecorByTaskId.size(); + for (int i = 0; i < decorCount; i++) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i); + if (decoration.mTaskInfo.displayId != displayId) continue; + decoration.onExclusionRegionChanged(exclusionRegion); + } + } + private void openHandleMenu(int taskId) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) @@ -750,10 +762,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, /** * Whether to pilfer the next motion event to send cancellations to the windows below. - * Useful when the caption window is spy and the gesture should be handle by the system + * Useful when the caption window is spy and the gesture should be handled by the system * instead of by the app for their custom header content. + * Should not have any effect when {@link Flags#enableAccessibleCustomHeaders()}, because + * a spy window is not used then. */ - private boolean mShouldPilferCaptionEvents; + private boolean mIsCustomHeaderGesture; + private boolean mIsResizeGesture; private boolean mIsDragging; private boolean mTouchscreenInUse; private boolean mHasLongClicked; @@ -867,7 +882,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // to offset position relative to caption as a whole. int[] viewLocation = new int[2]; v.getLocationInWindow(viewLocation); - final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e, + mIsResizeGesture = decoration.shouldResizeListenerHandleEvent(e, new Point(viewLocation[0], viewLocation[1])); // The caption window may be a spy window when the caption background is // transparent, which means events will fall through to the app window. Make @@ -875,21 +890,23 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // customizable region and what the app reported as exclusion areas, because // the drag-move or other caption gestures should take priority outside those // regions. - mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion - && downInExclusionRegion && isTransparentCaption) && !isResizeEvent; + mIsCustomHeaderGesture = downInCustomizableCaptionRegion + && downInExclusionRegion && isTransparentCaption; } - if (!mShouldPilferCaptionEvents) { - // The event will be handled by a window below or pilfered by resize handler. + if (mIsCustomHeaderGesture || mIsResizeGesture) { + // The event will be handled by the custom window below or pilfered by resize + // handler. return false; } - // Otherwise pilfer so that windows below receive cancellations for this gesture, and - // continue normal handling as a caption gesture. - if (mInputManager != null) { + if (mInputManager != null + && !Flags.enableAccessibleCustomHeaders()) { + // Pilfer so that windows below receive cancellations for this gesture. mInputManager.pilferPointers(v.getViewRootImpl().getInputToken()); } if (isUpOrCancel) { // Gesture is finished, reset state. - mShouldPilferCaptionEvents = false; + mIsCustomHeaderGesture = false; + mIsResizeGesture = false; } if (isAppHandle) { return mHandleDragDetector.onMotionEvent(v, e); @@ -1234,7 +1251,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, relevantDecor.updateHoverAndPressStatus(ev); final int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - if (!mTransitionDragActive && !Flags.enableHandleInputFix()) { + if (!mTransitionDragActive && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { relevantDecor.closeHandleMenuIfNeeded(ev); } } @@ -1277,7 +1294,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } final boolean shouldStartTransitionDrag = relevantDecor.checkTouchEventInFocusedCaptionHandle(ev) - || Flags.enableHandleInputFix(); + || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue(); if (dragFromStatusBarAllowed && shouldStartTransitionDrag) { mTransitionDragActive = true; } @@ -1592,8 +1609,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, windowDecoration.setDragPositioningCallback(taskPositioner); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, - mFocusTransitionObserver.hasGlobalFocus(taskInfo)); - if (!Flags.enableHandleInputFix()) { + mFocusTransitionObserver.hasGlobalFocus(taskInfo), + mExclusionRegion); + if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { incrementEventReceiverTasks(taskInfo.displayId); } } @@ -1618,6 +1636,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive); pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay); pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId); + pw.println(innerPrefix + "mExclusionRegion=" + mExclusionRegion); } private class DesktopModeOnTaskRepositionAnimationListener @@ -1754,7 +1773,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, && Flags.enableDesktopWindowingImmersiveHandleHiding()) { decor.onInsetsStateChanged(insetsState); } - if (!Flags.enableHandleInputFix()) { + if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { // If status bar inset is visible, top task is not in immersive mode. // This value is only needed when the App Handle input is being handled // through the global input monitor (hence the flag check) to ignore gestures diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index d97632a9428c..cdcf14e0cbf3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -394,7 +394,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } @Override - void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) { + void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); // The visibility, crop and position of the task should only be set when a task is // fluid resizing. In all other cases, it is expected that the transition handler sets @@ -415,7 +416,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // causes flickering. See b/270202228. final boolean applyTransactionOnDraw = taskInfo.isFreeform(); relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, - hasGlobalFocus); + hasGlobalFocus, displayExclusionRegion); if (!applyTransactionOnDraw) { t.apply(); } @@ -442,18 +443,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); if (taskInfo.isFreeform()) { // The Task is in Freeform mode -> show its header in sync since it's an integral part // of the window itself - a delayed header might cause bad UX. relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); } else { // The Task is outside Freeform mode -> allow the handle view to be delayed since the // handle is just a small addition to the window. relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); } Trace.endSection(); } @@ -462,11 +463,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) { // Clear the current ViewHost runnable as we will update the ViewHost here clearCurrentViewHostRunnable(); updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); if (mResult.mRootView != null) { updateViewHost(mRelayoutParams, startT, mResult); } @@ -489,7 +490,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { if (applyStartTransactionOnDraw) { throw new IllegalArgumentException( "We cannot both sync viewhost ondraw and delay viewhost creation."); @@ -498,7 +500,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin clearCurrentViewHostRunnable(); updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, shouldSetTaskVisibilityPositionAndCrop, - hasGlobalFocus); + hasGlobalFocus, displayExclusionRegion); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. @@ -513,7 +515,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) { Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces"); if (Flags.enableDesktopWindowingAppToWeb()) { setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp); @@ -538,7 +540,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, - mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus); + mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, + displayExclusionRegion); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -628,13 +631,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @Nullable private Intent getBrowserLink() { - // Do not show browser link in browser applications - final ComponentName baseActivity = mTaskInfo.baseActivity; - if (baseActivity != null && AppToWebUtils.isBrowserApp(mContext, - baseActivity.getPackageName(), mUserContext.getUserId())) { - return null; - } - final Uri browserLink; // If the captured link is available and has not expired, return the captured link. // Otherwise, return the generic link which is set to null if a generic link is unavailable. @@ -651,6 +647,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } + @Nullable + private Intent getAppLink() { + return mWebUri == null ? null + : AppToWebUtils.getAppIntent(mWebUri, mContext.getPackageManager()); + } + + private boolean isBrowserApp() { + final ComponentName baseActivity = mTaskInfo.baseActivity; + return baseActivity != null && AppToWebUtils.isBrowserApp(mContext, + baseActivity.getPackageName(), mUserContext.getUserId()); + } + UserHandle getUser() { return mUserContext.getUser(); } @@ -807,7 +815,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void disposeStatusBarInputLayer() { if (!isAppHandle(mWindowDecorViewHolder) - || !Flags.enableHandleInputFix()) { + || !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { return; } asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer(); @@ -874,7 +882,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean isKeyguardVisibleAndOccluded, boolean inFullImmersiveMode, @NonNull InsetsState displayInsetsState, - boolean hasGlobalFocus) { + boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); final boolean isAppHeader = captionLayoutId == R.layout.desktop_mode_app_header; @@ -885,6 +894,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId); relayoutParams.mHasGlobalFocus = hasGlobalFocus; + relayoutParams.mDisplayExclusionRegion.set(displayExclusionRegion); final boolean showCaption; if (Flags.enableFullyImmersiveInDesktop()) { @@ -910,10 +920,20 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mIsInsetSource = isAppHeader && !inFullImmersiveMode; if (isAppHeader) { if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { - // If the app is requesting to customize the caption bar, allow input to fall - // through to the windows below so that the app can respond to input events on - // their custom content. - relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + // The app is requesting to customize the caption bar, which means input on + // customizable/exclusion regions must go to the app instead of to the system. + // This may be accomplished with spy windows or custom touchable regions: + if (Flags.enableAccessibleCustomHeaders()) { + // Set the touchable region of the caption to only the areas where input should + // be handled by the system (i.e. non custom-excluded areas). The region will + // be calculated based on occluding caption elements and exclusion areas + // reported by the app. + relayoutParams.mLimitTouchRegionToSystemAreas = true; + } else { + // Allow input to fall through to the windows below so that the app can respond + // to input events on their custom content. + relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + } } else { if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue()) { // Force-consume the caption bar insets when the app tries to hide the caption. @@ -951,7 +971,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; relayoutParams.mOccludingCaptionElements.add(controlsElement); - } else if (isAppHandle && !Flags.enableHandleInputFix()) { + } else if (isAppHandle && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { // The focused decor (fullscreen/split) does not need to handle input because input in // the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel. // Note: This does not apply with the above flag enabled as the status bar input layer @@ -1368,6 +1388,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .shouldShowChangeAspectRatioButton(mTaskInfo); final boolean inDesktopImmersive = mDesktopRepository .isTaskInFullImmersiveState(mTaskInfo.taskId); + final boolean isBrowserApp = isBrowserApp(); mHandleMenu = mHandleMenuFactory.create( this, mWindowManagerWrapper, @@ -1379,7 +1400,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin supportsMultiInstance, shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, - getBrowserLink(), + isBrowserApp, + isBrowserApp ? getAppLink() : getBrowserLink(), mResult.mCaptionWidth, mResult.mCaptionHeight, mResult.mCaptionX, @@ -1560,13 +1582,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) { if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder) - || Flags.enableHandleInputFix()) { + || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { return false; } // The status bar input layer can only receive input in handle coordinates to begin with, // so checking coordinates is unnecessary as input is always within handle bounds. if (isAppHandle(mWindowDecorViewHolder) - && Flags.enableHandleInputFix() + && DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue() && isCaptionVisible()) { return true; } @@ -1603,7 +1625,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare */ void checkTouchEvent(MotionEvent ev) { - if (mResult.mRootView == null || Flags.enableHandleInputFix()) return; + if (mResult.mRootView == null || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return; final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); final View handle = caption.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() @@ -1616,7 +1638,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // If the whole handle menu can be touched directly, rely on FLAG_WATCH_OUTSIDE_TOUCH. // This is for the case that some of the handle menu is underneath the status bar. if (isAppHandle(mWindowDecorViewHolder) - && !Flags.enableHandleInputFix()) { + && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { mHandleMenu.checkMotionEvent(ev); closeHandleMenuIfNeeded(ev); } @@ -1630,7 +1652,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare against. */ void updateHoverAndPressStatus(MotionEvent ev) { - if (mResult.mRootView == null || Flags.enableHandleInputFix()) return; + if (mResult.mRootView == null || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return; final View handle = mResult.mRootView.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() && checkTouchEventInFocusedCaptionHandle(ev); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 2edc380756ac..54c247bff984 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -39,12 +39,14 @@ import android.widget.Button import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView +import android.window.DesktopModeFlags import android.window.SurfaceSyncGroup +import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.compose.ui.graphics.toArgb import androidx.core.view.isGone -import com.android.window.flags.Flags import com.android.wm.shell.R +import com.android.wm.shell.apptoweb.isBrowserApp import com.android.wm.shell.shared.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer @@ -73,7 +75,8 @@ class HandleMenu( private val shouldShowNewWindowButton: Boolean, private val shouldShowManageWindowsButton: Boolean, private val shouldShowChangeAspectRatioButton: Boolean, - private val openInBrowserIntent: Intent?, + private val isBrowserApp: Boolean, + private val openInAppOrBrowserIntent: Intent?, private val captionWidth: Int, private val captionHeight: Int, captionX: Int, @@ -83,7 +86,7 @@ class HandleMenu( private val taskInfo: RunningTaskInfo = parentDecor.mTaskInfo private val isViewAboveStatusBar: Boolean - get() = (Flags.enableHandleInputFix() && !taskInfo.isFreeform) + get() = (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue() && !taskInfo.isFreeform) private val pillElevation: Int = loadDimensionPixelSize( R.dimen.desktop_mode_handle_menu_pill_elevation) @@ -111,7 +114,7 @@ class HandleMenu( private val globalMenuPosition: Point = Point() private val shouldShowBrowserPill: Boolean - get() = openInBrowserIntent != null + get() = openInAppOrBrowserIntent != null private val shouldShowMoreActionsPill: Boolean get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton || @@ -128,7 +131,7 @@ class HandleMenu( onNewWindowClickListener: () -> Unit, onManageWindowsClickListener: () -> Unit, onChangeAspectRatioClickListener: () -> Unit, - openInBrowserClickListener: (Intent) -> Unit, + openInAppOrBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, @@ -146,7 +149,7 @@ class HandleMenu( onNewWindowClickListener = onNewWindowClickListener, onManageWindowsClickListener = onManageWindowsClickListener, onChangeAspectRatioClickListener = onChangeAspectRatioClickListener, - openInBrowserClickListener = openInBrowserClickListener, + openInAppOrBrowserClickListener = openInAppOrBrowserClickListener, onOpenByDefaultClickListener = onOpenByDefaultClickListener, onCloseMenuClickListener = onCloseMenuClickListener, onOutsideTouchListener = onOutsideTouchListener, @@ -167,7 +170,7 @@ class HandleMenu( onNewWindowClickListener: () -> Unit, onManageWindowsClickListener: () -> Unit, onChangeAspectRatioClickListener: () -> Unit, - openInBrowserClickListener: (Intent) -> Unit, + openInAppOrBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, @@ -181,7 +184,8 @@ class HandleMenu( shouldShowBrowserPill = shouldShowBrowserPill, shouldShowNewWindowButton = shouldShowNewWindowButton, shouldShowManageWindowsButton = shouldShowManageWindowsButton, - shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton + shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton, + isBrowserApp = isBrowserApp ).apply { bind(taskInfo, appIconBitmap, appName, shouldShowMoreActionsPill) this.onToDesktopClickListener = onToDesktopClickListener @@ -190,8 +194,8 @@ class HandleMenu( this.onNewWindowClickListener = onNewWindowClickListener this.onManageWindowsClickListener = onManageWindowsClickListener this.onChangeAspectRatioClickListener = onChangeAspectRatioClickListener - this.onOpenInBrowserClickListener = { - openInBrowserClickListener.invoke(openInBrowserIntent!!) + this.onOpenInAppOrBrowserClickListener = { + openInAppOrBrowserClickListener.invoke(openInAppOrBrowserIntent!!) } this.onOpenByDefaultClickListener = onOpenByDefaultClickListener this.onCloseMenuClickListener = onCloseMenuClickListener @@ -201,7 +205,8 @@ class HandleMenu( val x = handleMenuPosition.x.toInt() val y = handleMenuPosition.y.toInt() handleMenuViewContainer = - if ((!taskInfo.isFreeform && Flags.enableHandleInputFix()) || forceShowSystemBars) { + if ((!taskInfo.isFreeform && DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) + || forceShowSystemBars) { AdditionalSystemViewContainer( windowManagerWrapper = windowManagerWrapper, taskId = taskInfo.taskId, @@ -237,7 +242,7 @@ class HandleMenu( menuX = marginMenuStart menuY = captionY + marginMenuTop } else { - if (Flags.enableHandleInputFix()) { + if (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { // In a focused decor, we use global coordinates for handle menu. Therefore we // need to account for other factors like split stage and menu/handle width to // center the menu. @@ -435,14 +440,15 @@ class HandleMenu( /** The view within the Handle Menu, with options to change the windowing mode and more. */ @SuppressLint("ClickableViewAccessibility") class HandleMenuView( - context: Context, + private val context: Context, menuWidth: Int, captionHeight: Int, private val shouldShowWindowingPill: Boolean, private val shouldShowBrowserPill: Boolean, private val shouldShowNewWindowButton: Boolean, private val shouldShowManageWindowsButton: Boolean, - private val shouldShowChangeAspectRatioButton: Boolean + private val shouldShowChangeAspectRatioButton: Boolean, + private val isBrowserApp: Boolean ) { val rootView = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View @@ -472,11 +478,12 @@ class HandleMenu( private val changeAspectRatioBtn = moreActionsPill .requireViewById<Button>(R.id.change_aspect_ratio_button) - // Open in Browser Pill. - private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill) - private val browserBtn = openInBrowserPill.requireViewById<Button>( - R.id.open_in_browser_button) - private val openByDefaultBtn = openInBrowserPill.requireViewById<ImageButton>( + // Open in Browser/App Pill. + private val openInAppOrBrowserPill = rootView.requireViewById<View>( + R.id.open_in_app_or_browser_pill) + private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<Button>( + R.id.open_in_app_or_browser_button) + private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>( R.id.open_by_default_button) private val decorThemeUtil = DecorThemeUtil(context) private val animator = HandleMenuAnimator(rootView, menuWidth, captionHeight.toFloat()) @@ -490,7 +497,7 @@ class HandleMenu( var onNewWindowClickListener: (() -> Unit)? = null var onManageWindowsClickListener: (() -> Unit)? = null var onChangeAspectRatioClickListener: (() -> Unit)? = null - var onOpenInBrowserClickListener: (() -> Unit)? = null + var onOpenInAppOrBrowserClickListener: (() -> Unit)? = null var onOpenByDefaultClickListener: (() -> Unit)? = null var onCloseMenuClickListener: (() -> Unit)? = null var onOutsideTouchListener: (() -> Unit)? = null @@ -499,7 +506,7 @@ class HandleMenu( fullscreenBtn.setOnClickListener { onToFullscreenClickListener?.invoke() } splitscreenBtn.setOnClickListener { onToSplitScreenClickListener?.invoke() } desktopBtn.setOnClickListener { onToDesktopClickListener?.invoke() } - browserBtn.setOnClickListener { onOpenInBrowserClickListener?.invoke() } + openInAppOrBrowserBtn.setOnClickListener { onOpenInAppOrBrowserClickListener?.invoke() } openByDefaultBtn.setOnClickListener { onOpenByDefaultClickListener?.invoke() } @@ -535,10 +542,10 @@ class HandleMenu( if (shouldShowMoreActionsPill) { bindMoreActionsPill(style) } - bindOpenInBrowserPill(style) + bindOpenInAppOrBrowserPill(style) } - /** Animates the menu opening. */ + /** Animates the menu openInAppOrBrowserg. */ fun animateOpenMenu() { if (taskInfo.isFullscreen || taskInfo.isMultiWindow) { animator.animateCaptionHandleExpandToOpen() @@ -660,13 +667,20 @@ class HandleMenu( } } - private fun bindOpenInBrowserPill(style: MenuStyle) { - openInBrowserPill.apply { + private fun bindOpenInAppOrBrowserPill(style: MenuStyle) { + openInAppOrBrowserPill.apply { isGone = !shouldShowBrowserPill background.setTint(style.backgroundColor) } - browserBtn.apply { + val btnText = if (isBrowserApp) { + getString(R.string.open_in_app_text) + } else { + getString(R.string.open_in_browser_text) + } + openInAppOrBrowserBtn.apply { + text = btnText + contentDescription = btnText setTextColor(style.textColor) compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } @@ -674,6 +688,8 @@ class HandleMenu( openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor) } + private fun getString(@StringRes resId: Int): String = context.resources.getString(resId) + private data class MenuStyle( @ColorInt val backgroundColor: Int, @ColorInt val textColor: Int, @@ -708,7 +724,8 @@ interface HandleMenuFactory { shouldShowNewWindowButton: Boolean, shouldShowManageWindowsButton: Boolean, shouldShowChangeAspectRatioButton: Boolean, - openInBrowserIntent: Intent?, + isBrowserApp: Boolean, + openInAppOrBrowserIntent: Intent?, captionWidth: Int, captionHeight: Int, captionX: Int, @@ -729,7 +746,8 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowNewWindowButton: Boolean, shouldShowManageWindowsButton: Boolean, shouldShowChangeAspectRatioButton: Boolean, - openInBrowserIntent: Intent?, + isBrowserApp: Boolean, + openInAppOrBrowserIntent: Intent?, captionWidth: Int, captionHeight: Int, captionX: Int, @@ -746,7 +764,8 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowNewWindowButton, shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, - openInBrowserIntent, + isBrowserApp, + openInAppOrBrowserIntent, captionWidth, captionHeight, captionX, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 0c475f12f53b..470e5a1d88b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -74,7 +74,8 @@ class HandleMenuAnimator( private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill) private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill) private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill) - private val openInBrowserPill: ViewGroup = handleMenu.requireViewById(R.id.open_in_browser_pill) + private val openInAppOrBrowserPill: ViewGroup = + handleMenu.requireViewById(R.id.open_in_app_or_browser_pill) /** Animates the opening of the handle menu. */ fun animateOpen() { @@ -83,7 +84,7 @@ class HandleMenuAnimator( animateAppInfoPillOpen() animateWindowingPillOpen() animateMoreActionsPillOpen() - animateOpenInBrowserPill() + animateOpenInAppOrBrowserPill() runAnimations { appInfoPill.post { appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( @@ -103,7 +104,7 @@ class HandleMenuAnimator( animateAppInfoPillOpen() animateWindowingPillOpen() animateMoreActionsPillOpen() - animateOpenInBrowserPill() + animateOpenInAppOrBrowserPill() runAnimations { appInfoPill.post { appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( @@ -124,7 +125,7 @@ class HandleMenuAnimator( animateAppInfoPillFadeOut() windowingPillClose() moreActionsPillClose() - openInBrowserPillClose() + openInAppOrBrowserPillClose() runAnimations(after) } @@ -141,7 +142,7 @@ class HandleMenuAnimator( animateAppInfoPillFadeOut() windowingPillClose() moreActionsPillClose() - openInBrowserPillClose() + openInAppOrBrowserPillClose() runAnimations(after) } @@ -154,7 +155,7 @@ class HandleMenuAnimator( appInfoPill.children.forEach { it.alpha = 0f } windowingPill.alpha = 0f moreActionsPill.alpha = 0f - openInBrowserPill.alpha = 0f + openInAppOrBrowserPill.alpha = 0f // Setup pivots. handleMenu.pivotX = menuWidth / 2f @@ -166,8 +167,8 @@ class HandleMenuAnimator( moreActionsPill.pivotX = menuWidth / 2f moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat() - openInBrowserPill.pivotX = menuWidth / 2f - openInBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat() + openInAppOrBrowserPill.pivotX = menuWidth / 2f + openInAppOrBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat() } private fun animateAppInfoPillOpen() { @@ -297,36 +298,36 @@ class HandleMenuAnimator( } } - private fun animateOpenInBrowserPill() { + private fun animateOpenInAppOrBrowserPill() { // Open in Browser X & Y Scaling Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { startDelay = BODY_SCALE_OPEN_DELAY duration = BODY_SCALE_OPEN_DURATION } animators += - ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { startDelay = BODY_SCALE_OPEN_DELAY duration = BODY_SCALE_OPEN_DURATION } // Open in Browser Opacity Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 1f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 1f).apply { startDelay = BODY_ALPHA_OPEN_DELAY duration = BODY_ALPHA_OPEN_DURATION } // Open in Browser Elevation Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Z, 1f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Z, 1f).apply { startDelay = ELEVATION_OPEN_DELAY duration = BODY_ELEVATION_OPEN_DURATION } // Open in Browser Button Opacity Animation - val button = openInBrowserPill.requireViewById<Button>(R.id.open_in_browser_button) + val button = openInAppOrBrowserPill.requireViewById<Button>(R.id.open_in_app_or_browser_button) animators += ObjectAnimator.ofFloat(button, ALPHA, 1f).apply { startDelay = BODY_ALPHA_OPEN_DELAY @@ -438,33 +439,33 @@ class HandleMenuAnimator( } } - private fun openInBrowserPillClose() { + private fun openInAppOrBrowserPillClose() { // Open in Browser X & Y Scaling Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply { duration = BODY_CLOSE_DURATION } animators += - ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply { duration = BODY_CLOSE_DURATION } // Open in Browser Opacity Animation animators += - ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply { duration = BODY_CLOSE_DURATION } animators += - ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply { duration = BODY_CLOSE_DURATION } // Upward Open in Browser y-translation Animation val yStart: Float = -captionHeight / 2 animators += - ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Y, yStart).apply { + ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Y, yStart).apply { duration = BODY_CLOSE_DURATION } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt index cf82bb4f9919..8bc56e0807a8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt @@ -16,13 +16,12 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager.RunningTaskInfo -import com.android.window.flags.Flags -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer - import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.widget.ImageButton +import android.window.DesktopModeFlags +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer /** * A custom [ImageButton] for buttons inside handle menu that intentionally doesn't handle hovers. @@ -39,7 +38,7 @@ class HandleMenuImageButton( lateinit var taskInfo: RunningTaskInfo override fun onHoverEvent(motionEvent: MotionEvent): Boolean { - if (Flags.enableHandleInputFix() || taskInfo.isFreeform) { + if (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue() || taskInfo.isFreeform) { return super.onHoverEvent(motionEvent) } else { return false 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 b016c755e323..a3c75bf33cde 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 @@ -127,7 +127,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } mDisplayController.removeDisplayWindowListener(this); - relayout(mTaskInfo, mHasGlobalFocus); + relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion); } }; @@ -143,7 +143,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControl mDecorationContainerSurface; SurfaceControl mCaptionContainerSurface; - private WindowlessWindowManager mCaptionWindowManager; + private CaptionWindowlessWindowManager mCaptionWindowManager; private SurfaceControlViewHost mViewHost; private Configuration mWindowDecorConfig; TaskDragResizer mTaskDragResizer; @@ -152,6 +152,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> boolean mIsStatusBarVisible; boolean mIsKeyguardVisibleAndOccluded; boolean mHasGlobalFocus; + final Region mExclusionRegion = Region.obtain(); /** The most recent set of insets applied to this window decoration. */ private WindowDecorationInsets mWindowDecorationInsets; @@ -218,7 +219,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * constructor. * @param hasGlobalFocus Whether the task is focused */ - abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus); + abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion); /** * Used by the {@link DragPositioningCallback} associated with the implementing class to @@ -244,6 +246,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mTaskInfo = params.mRunningTaskInfo; } mHasGlobalFocus = params.mHasGlobalFocus; + mExclusionRegion.set(params.mDisplayExclusionRegion); final int oldLayoutResId = mLayoutResId; mLayoutResId = params.mLayoutResId; @@ -402,7 +405,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final int elementWidthPx = resources.getDimensionPixelSize(element.mWidthResId); boundingRects[i] = - calculateBoundingRect(element, elementWidthPx, captionInsetsRect); + calculateBoundingRectLocal(element, elementWidthPx, captionInsetsRect); // Subtract the regions used by the caption elements, the rest is // customizable. if (params.hasInputFeatureSpy()) { @@ -477,9 +480,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. - mCaptionWindowManager = new WindowlessWindowManager( - mTaskInfo.getConfiguration(), mCaptionContainerSurface, - null /* hostInputToken */); + mCaptionWindowManager = new CaptionWindowlessWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface); } mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration()); final WindowManager.LayoutParams lp = @@ -492,6 +494,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> lp.setTitle("Caption of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); lp.inputFeatures = params.mInputFeatures; + final Rect localCaptionBounds = new Rect( + outResult.mCaptionX, + outResult.mCaptionY, + outResult.mCaptionX + outResult.mCaptionWidth, + outResult.mCaptionY + outResult.mCaptionHeight); + final Region touchableRegion = params.mLimitTouchRegionToSystemAreas + ? calculateLimitedTouchableRegion(params, localCaptionBounds) + : null; if (mViewHost == null) { Trace.beginSection("CaptionViewHostLayout-new"); mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay, @@ -503,6 +513,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); } outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0); + if (params.mLimitTouchRegionToSystemAreas) { + mCaptionWindowManager.setTouchRegion(mViewHost, touchableRegion); + } mViewHost.setView(outResult.mRootView, lp); Trace.endSection(); } else { @@ -514,13 +527,71 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); } outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0); + if (params.mLimitTouchRegionToSystemAreas) { + mCaptionWindowManager.setTouchRegion(mViewHost, touchableRegion); + } mViewHost.relayout(lp); Trace.endSection(); } + if (touchableRegion != null) { + touchableRegion.recycle(); + } Trace.endSection(); // CaptionViewHostLayout } - private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element, + @NonNull + private Region calculateLimitedTouchableRegion( + RelayoutParams params, + @NonNull Rect localCaptionBounds) { + // Make caption bounds relative to display to align with exclusion region. + final Point positionInParent = params.mRunningTaskInfo.positionInParent; + final Rect captionBoundsInDisplay = new Rect(localCaptionBounds); + captionBoundsInDisplay.offsetTo(positionInParent.x, positionInParent.y); + + final Region boundingRects = calculateBoundingRectsRegion(params, captionBoundsInDisplay); + + final Region customizedRegion = Region.obtain(); + customizedRegion.set(captionBoundsInDisplay); + customizedRegion.op(boundingRects, Region.Op.DIFFERENCE); + customizedRegion.op(params.mDisplayExclusionRegion, Region.Op.INTERSECT); + + final Region touchableRegion = Region.obtain(); + touchableRegion.set(captionBoundsInDisplay); + touchableRegion.op(customizedRegion, Region.Op.DIFFERENCE); + // Return resulting region back to window coordinates. + touchableRegion.translate(-positionInParent.x, -positionInParent.y); + + boundingRects.recycle(); + customizedRegion.recycle(); + return touchableRegion; + } + + @NonNull + private Region calculateBoundingRectsRegion( + @NonNull RelayoutParams params, + @NonNull Rect captionBoundsInDisplay) { + final int numOfElements = params.mOccludingCaptionElements.size(); + final Region region = Region.obtain(); + if (numOfElements == 0) { + // The entire caption is a bounding rect. + region.set(captionBoundsInDisplay); + return region; + } + final Resources resources = mDecorWindowContext.getResources(); + for (int i = 0; i < numOfElements; i++) { + final OccludingCaptionElement element = params.mOccludingCaptionElements.get(i); + final int elementWidthPx = resources.getDimensionPixelSize(element.mWidthResId); + final Rect boundingRect = calculateBoundingRectLocal(element, elementWidthPx, + captionBoundsInDisplay); + // Bounding rect is initially calculated relative to the caption, so offset it to make + // it relative to the display. + boundingRect.offset(captionBoundsInDisplay.left, captionBoundsInDisplay.top); + region.union(boundingRect); + } + return region; + } + + private Rect calculateBoundingRectLocal(@NonNull OccludingCaptionElement element, int elementWidthPx, @NonNull Rect captionRect) { switch (element.mAlignment) { case START -> { @@ -539,7 +610,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mIsKeyguardVisibleAndOccluded = visible && occluded; final boolean changed = prevVisAndOccluded != mIsKeyguardVisibleAndOccluded; if (changed) { - relayout(mTaskInfo, mHasGlobalFocus); + relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion); } } @@ -549,10 +620,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final boolean changed = prevStatusBarVisibility != mIsStatusBarVisible; if (changed) { - relayout(mTaskInfo, mHasGlobalFocus); + relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion); } } + void onExclusionRegionChanged(@NonNull Region exclusionRegion) { + relayout(mTaskInfo, mHasGlobalFocus, exclusionRegion); + } + /** * Update caption visibility state and views. */ @@ -751,9 +826,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mCaptionHeightId; int mCaptionWidthId; final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>(); + boolean mLimitTouchRegionToSystemAreas; int mInputFeatures; boolean mIsInsetSource = true; @InsetsSource.Flags int mInsetSourceFlags; + final Region mDisplayExclusionRegion = Region.obtain(); int mShadowRadiusId; int mCornerRadius; @@ -772,9 +849,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mCaptionHeightId = Resources.ID_NULL; mCaptionWidthId = Resources.ID_NULL; mOccludingCaptionElements.clear(); + mLimitTouchRegionToSystemAreas = false; mInputFeatures = 0; mIsInsetSource = true; mInsetSourceFlags = 0; + mDisplayExclusionRegion.setEmpty(); mShadowRadiusId = Resources.ID_NULL; mCornerRadius = 0; @@ -830,6 +909,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } + private static class CaptionWindowlessWindowManager extends WindowlessWindowManager { + CaptionWindowlessWindowManager( + @NonNull Configuration configuration, + @NonNull SurfaceControl rootSurface) { + super(configuration, rootSurface, /* hostInputToken= */ null); + } + + /** Set the view host's touchable region. */ + void setTouchRegion(@NonNull SurfaceControlViewHost viewHost, @NonNull Region region) { + setTouchRegion(viewHost.getWindowToken().asBinder(), region); + } + } + @VisibleForTesting public interface SurfaceControlViewHostFactory { default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index b5700ffb046b..503ad92d4d71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -34,15 +34,14 @@ import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageButton +import android.window.DesktopModeFlags import androidx.core.view.ViewCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import com.android.internal.policy.SystemBarUtils -import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.shared.animation.Interpolators import com.android.wm.shell.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer -import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data /** * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). @@ -141,7 +140,7 @@ internal class AppHandleViewHolder( private fun createStatusBarInputLayer(handlePosition: Point, handleWidth: Int, handleHeight: Int) { - if (!Flags.enableHandleInputFix()) return + if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper, taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index 4fe66f3357a3..4cddf31321d6 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -23,8 +23,9 @@ import android.tools.flicker.assertors.assertions.AppLayerIncreasesInSize import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart -import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible import android.tools.flicker.assertors.assertions.AppWindowAlignsWithOnlyOneDisplayCornerAtEnd +import android.tools.flicker.assertors.assertions.AppWindowBecomesInvisible +import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible import android.tools.flicker.assertors.assertions.AppWindowCoversLeftHalfScreenAtEnd import android.tools.flicker.assertors.assertions.AppWindowCoversRightHalfScreenAtEnd import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd @@ -44,6 +45,7 @@ import android.tools.flicker.assertors.assertions.LauncherWindowReplacesAppAsTop import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfigEntry import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.common.Components.LAUNCHER import android.tools.flicker.config.desktopmode.Components.DESKTOP_MODE_APP import android.tools.flicker.config.desktopmode.Components.DESKTOP_WALLPAPER import android.tools.flicker.config.desktopmode.Components.NON_RESIZABLE_APP @@ -365,5 +367,57 @@ class DesktopModeFlickerScenarios { AppWindowAlignsWithOnlyOneDisplayCornerAtEnd(DESKTOP_MODE_APP) ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) + + val MINIMIZE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("MINIMIZE_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions + .filter { it.type == TransitionType.MINIMIZE } + .sortedByDescending { it.id } + .drop(1) + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(DESKTOP_MODE_APP), + AppWindowBecomesInvisible(DESKTOP_MODE_APP), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) + + val MINIMIZE_LAST_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("MINIMIZE_LAST_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + val lastTransition = + transitions + .filter { it.type == TransitionType.MINIMIZE } + .maxByOrNull { it.id }!! + return listOf(lastTransition) + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(DESKTOP_MODE_APP), + AppWindowBecomesInvisible(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(LAUNCHER), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) } } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt new file mode 100644 index 000000000000..58582b02c212 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP +import com.android.wm.shell.scenarios.MinimizeAppWindows +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Minimize app windows by pressing the minimize button. + * + * Assert that the app windows gets hidden. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MinimizeAppsLandscape : MinimizeAppWindows(rotation = ROTATION_90) { + @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"]) + @Test + override fun minimizeAllAppWindows() = super.minimizeAllAppWindows() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(MINIMIZE_APP) + .use(MINIMIZE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt new file mode 100644 index 000000000000..7970426a6ee8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP +import com.android.wm.shell.scenarios.MinimizeAppWindows +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Minimize app windows by pressing the minimize button. + * + * Assert that the app windows gets hidden. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MinimizeAppsPortrait : MinimizeAppWindows() { + @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"]) + @Test + override fun minimizeAllAppWindows() = super.minimizeAllAppWindows() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(MINIMIZE_APP) + .use(MINIMIZE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt index 824c4482c1e6..f442fdb31592 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.scenarios import android.tools.NavBar import android.tools.Rotation +import com.android.internal.R import com.android.window.flags.Flags import com.android.wm.shell.Utils import org.junit.After @@ -40,6 +41,9 @@ constructor( @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + // Skip the test when the drag-to-maximize is enabled on this device. + Assume.assumeFalse(Flags.enableDragToMaximize() && + instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode)) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) testApp.enterDesktopWithDrag(wmHelper, device) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java index d38b848fbb4d..329a10998f23 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java @@ -16,9 +16,8 @@ package com.android.wm.shell.bubbles.bar; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; -import android.graphics.drawable.ColorDrawable; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -47,10 +46,9 @@ public class BubbleBarHandleViewTest extends ShellTestCase { public void testUpdateHandleColor_lightBg() { mHandleView.updateHandleColor(false /* isRegionDark */, false /* animated */); - assertTrue(mHandleView.getClipToOutline()); - assertTrue(mHandleView.getBackground() instanceof ColorDrawable); - ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground(); - assertEquals(bgDrawable.getColor(), + assertFalse(mHandleView.getClipToOutline()); + int handleColor = mHandleView.mHandlePaint.getColor(); + assertEquals(handleColor, ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_dark)); } @@ -58,10 +56,9 @@ public class BubbleBarHandleViewTest extends ShellTestCase { public void testUpdateHandleColor_darkBg() { mHandleView.updateHandleColor(true /* isRegionDark */, false /* animated */); - assertTrue(mHandleView.getClipToOutline()); - assertTrue(mHandleView.getBackground() instanceof ColorDrawable); - ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground(); - assertEquals(bgDrawable.getColor(), + assertFalse(mHandleView.getClipToOutline()); + int handleColor = mHandleView.mHandlePaint.getColor(); + assertEquals(handleColor, ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_light)); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java new file mode 100644 index 000000000000..b9490b881d08 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.pip; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Pair; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.ShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test against {@link PipAppOpsListener}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipAppOpsListenerTest { + + @Mock private Context mMockContext; + @Mock private PackageManager mMockPackageManager; + @Mock private AppOpsManager mMockAppOpsManager; + @Mock private PipAppOpsListener.Callback mMockCallback; + @Mock private ShellExecutor mMockExecutor; + + private PipAppOpsListener mPipAppOpsListener; + + private ArgumentCaptor<AppOpsManager.OnOpChangedListener> mOnOpChangedListenerCaptor; + private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; + private Pair<ComponentName, Integer> mTopPipActivity; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); + when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)) + .thenReturn(mMockAppOpsManager); + mOnOpChangedListenerCaptor = ArgumentCaptor.forClass( + AppOpsManager.OnOpChangedListener.class); + mRunnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + } + + @Test + public void onActivityPinned_registerAppOpsListener() { + String packageName = "com.android.test.pip"; + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + + mPipAppOpsListener.onActivityPinned(packageName); + + verify(mMockAppOpsManager).startWatchingMode( + eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName), + any(AppOpsManager.OnOpChangedListener.class)); + } + + @Test + public void onActivityUnpinned_unregisterAppOpsListener() { + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + + mPipAppOpsListener.onActivityUnpinned(); + + verify(mMockAppOpsManager).stopWatchingMode(any(AppOpsManager.OnOpChangedListener.class)); + } + + @Test + public void disablePipAppOps_dismissPip() throws PackageManager.NameNotFoundException { + String packageName = "com.android.test.pip"; + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + // Set up the top pip activity info as mTopPipActivity + mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0); + mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity); + // Set up the application info as mApplicationInfo + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = packageName; + when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + // Mock the mode to be **not** allowed + when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName))) + .thenReturn(AppOpsManager.MODE_DEFAULT); + // Set up the initial state + mPipAppOpsListener.onActivityPinned(packageName); + verify(mMockAppOpsManager).startWatchingMode( + eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName), + mOnOpChangedListenerCaptor.capture()); + AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue(); + + opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE), + packageName); + + verify(mMockExecutor).execute(mRunnableArgumentCaptor.capture()); + Runnable runnable = mRunnableArgumentCaptor.getValue(); + runnable.run(); + verify(mMockCallback).dismissPip(); + } + + @Test + public void disablePipAppOps_differentPackage_doNothing() + throws PackageManager.NameNotFoundException { + String packageName = "com.android.test.pip"; + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + // Set up the top pip activity info as mTopPipActivity + mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0); + mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity); + // Set up the application info as mApplicationInfo + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = packageName + ".modified"; + when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + // Mock the mode to be **not** allowed + when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName))) + .thenReturn(AppOpsManager.MODE_DEFAULT); + // Set up the initial state + mPipAppOpsListener.onActivityPinned(packageName); + verify(mMockAppOpsManager).startWatchingMode( + eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName), + mOnOpChangedListenerCaptor.capture()); + AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue(); + + opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE), + packageName); + + verifyZeroInteractions(mMockExecutor); + } + + @Test + public void disablePipAppOps_nameNotFound_unregisterAppOpsListener() + throws PackageManager.NameNotFoundException { + String packageName = "com.android.test.pip"; + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + // Set up the top pip activity info as mTopPipActivity + mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0); + mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity); + // Set up the application info as mApplicationInfo + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = packageName; + when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenThrow(PackageManager.NameNotFoundException.class); + // Mock the mode to be **not** allowed + when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName))) + .thenReturn(AppOpsManager.MODE_DEFAULT); + // Set up the initial state + mPipAppOpsListener.onActivityPinned(packageName); + verify(mMockAppOpsManager).startWatchingMode( + eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName), + mOnOpChangedListenerCaptor.capture()); + AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue(); + + opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE), + packageName); + + verify(mMockAppOpsManager).stopWatchingMode(any(AppOpsManager.OnOpChangedListener.class)); + } + + private Pair<ComponentName, Integer> getTopPipActivity(Context context) { + return mTopPipActivity; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt new file mode 100644 index 000000000000..6df8d6fd7717 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WindowingMode +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.SurfaceControl +import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CLOSE +import android.window.TransitionInfo +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.ShellExecutor +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { + + private val testExecutor = mock<ShellExecutor>() + private val closingTaskLeash = mock<SurfaceControl>() + private val displayController = mock<DisplayController>() + + private lateinit var handler: DesktopBackNavigationTransitionHandler + + @Before + fun setUp() { + handler = + DesktopBackNavigationTransitionHandler( + testExecutor, + testExecutor, + displayController + ) + whenever(displayController.getDisplayContext(any())).thenReturn(mContext) + } + + @Test + fun handleRequest_returnsNull() { + assertNull(handler.handleRequest(mock(), mock())) + } + + @Test + fun startAnimation_openTransition_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = + createTransitionInfo( + type = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate open transition", animates) + } + + @Test + fun startAnimation_toBackTransitionFullscreenTask_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate fullscreen task to back transition", animates) + } + + @Test + fun startAnimation_toBackTransitionOpeningFreeformTask_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = + createTransitionInfo( + changeMode = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate opening freeform task to back transition", animates) + } + + @Test + fun startAnimation_toBackTransitionToBackFreeformTask_returnsTrue() { + val animates = + handler.startAnimation( + transition = mock(), + info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertTrue("Should animate going to back freeform task close transition", animates) + } + + @Test + fun startAnimation_closeTransitionClosingFreeformTask_returnsTrue() { + val animates = + handler.startAnimation( + transition = mock(), + info = createTransitionInfo( + type = TRANSIT_CLOSE, + changeMode = TRANSIT_CLOSE, + task = createTask(WINDOWING_MODE_FREEFORM) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertTrue("Should animate going to back freeform task close transition", animates) + } + private fun createTransitionInfo( + type: Int = WindowManager.TRANSIT_TO_BACK, + changeMode: Int = WindowManager.TRANSIT_TO_BACK, + task: RunningTaskInfo + ): TransitionInfo = + TransitionInfo(type, 0 /* flags */).apply { + addChange( + TransitionInfo.Change(mock(), closingTaskLeash).apply { + mode = changeMode + parent = null + taskInfo = task + } + ) + } + + private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(windowingMode) + .build() +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt index e05a0b54fcf4..a4f4d05d2079 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt @@ -15,6 +15,7 @@ */ package com.android.wm.shell.desktopmode +import android.animation.AnimatorTestRule import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS import android.graphics.Rect @@ -24,6 +25,7 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner +import android.testing.TestableLooper import android.view.Display.DEFAULT_DISPLAY import android.view.Surface import android.view.SurfaceControl @@ -43,6 +45,7 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule @@ -64,17 +67,19 @@ import org.mockito.kotlin.whenever * Usage: atest WMShellUnitTests:DesktopImmersiveControllerTest */ @SmallTest +@TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner::class) class DesktopImmersiveControllerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() + @JvmField @Rule val animatorTestRule = AnimatorTestRule(this) @Mock private lateinit var mockTransitions: Transitions private lateinit var desktopRepository: DesktopRepository @Mock private lateinit var mockDisplayController: DisplayController @Mock private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer @Mock private lateinit var mockDisplayLayout: DisplayLayout - private val transactionSupplier = { SurfaceControl.Transaction() } + private val transactionSupplier = { StubTransaction() } private lateinit var controller: DesktopImmersiveController @@ -89,10 +94,12 @@ class DesktopImmersiveControllerTest : ShellTestCase() { (invocation.getArgument(0) as Rect).set(STABLE_BOUNDS) } controller = DesktopImmersiveController( + shellInit = mock(), transitions = mockTransitions, desktopRepository = desktopRepository, displayController = mockDisplayController, shellTaskOrganizer = mockShellTaskOrganizer, + shellCommandHandler = mock(), transactionSupplier = transactionSupplier, ) } @@ -672,6 +679,60 @@ class DesktopImmersiveControllerTest : ShellTestCase() { assertThat(controller.isImmersiveChange(transition, change)).isTrue() } + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun externalAnimateResizeChange_doesNotCleanUpPendingTransitionState() { + val task = createFreeformTask() + val mockBinder = mock(IBinder::class.java) + whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) + .thenReturn(mockBinder) + desktopRepository.setTaskInFullImmersiveState( + displayId = task.displayId, + taskId = task.taskId, + immersive = true + ) + + controller.moveTaskToNonImmersive(task) + + controller.animateResizeChange( + change = TransitionInfo.Change(task.token, SurfaceControl()).apply { + taskInfo = task + }, + startTransaction = StubTransaction(), + finishTransaction = StubTransaction(), + finishCallback = { } + ) + animatorTestRule.advanceTimeBy(DesktopImmersiveController.FULL_IMMERSIVE_ANIM_DURATION_MS) + + assertThat(controller.state).isNotNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun startAnimation_missingChange_clearsState() { + val task = createFreeformTask() + val mockBinder = mock(IBinder::class.java) + whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) + .thenReturn(mockBinder) + desktopRepository.setTaskInFullImmersiveState( + displayId = task.displayId, + taskId = task.taskId, + immersive = false + ) + + controller.moveTaskToImmersive(task) + + controller.startAnimation( + transition = mockBinder, + info = createTransitionInfo(changes = emptyList()), + startTransaction = StubTransaction(), + finishTransaction = StubTransaction(), + finishCallback = {} + ) + + assertThat(controller.state).isNull() + } + private fun createTransitionInfo( @TransitionType type: Int = TRANSIT_CHANGE, @TransitionFlags flags: Int = 0, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index b06c2dad4ffc..f21f26443748 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -32,6 +32,7 @@ import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl import android.view.WindowManager import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TransitionType import android.window.TransitionInfo import android.window.WindowContainerTransaction @@ -77,16 +78,28 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var transitions: Transitions - @Mock lateinit var desktopRepository: DesktopRepository - @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler - @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler - @Mock lateinit var desktopImmersiveController: DesktopImmersiveController - @Mock lateinit var interactionJankMonitor: InteractionJankMonitor - @Mock lateinit var mockHandler: Handler - @Mock lateinit var closingTaskLeash: SurfaceControl - @Mock lateinit var shellInit: ShellInit - @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock + lateinit var transitions: Transitions + @Mock + lateinit var desktopRepository: DesktopRepository + @Mock + lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler + @Mock + lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler + @Mock + lateinit var desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler + @Mock + lateinit var desktopImmersiveController: DesktopImmersiveController + @Mock + lateinit var interactionJankMonitor: InteractionJankMonitor + @Mock + lateinit var mockHandler: Handler + @Mock + lateinit var closingTaskLeash: SurfaceControl + @Mock + lateinit var shellInit: ShellInit + @Mock + lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer private lateinit var mixedHandler: DesktopMixedTransitionHandler @@ -100,6 +113,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { freeformTaskTransitionHandler, closeDesktopTaskTransitionHandler, desktopImmersiveController, + desktopBackNavigationTransitionHandler, interactionJankMonitor, mockHandler, shellInit, @@ -595,6 +609,87 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { assertThat(mixedHandler.pendingMixedTransitions).isEmpty() } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun startAnimation_withMinimizingDesktopTask_callsBackNavigationHandler() { + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) + whenever( + desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any()) + ) + .thenReturn(true) + mixedHandler.addPendingMixedTransition( + PendingMixedTransition.Minimize( + transition = transition, + minimizingTask = minimizingTask.taskId, + isLastTask = false, + ) + ) + + val minimizingTaskChange = createChange(minimizingTask) + val started = mixedHandler.startAnimation( + transition = transition, + info = + createTransitionInfo( + TRANSIT_TO_BACK, + listOf(minimizingTaskChange) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertTrue("Should delegate animation to back navigation transition handler", started) + verify(desktopBackNavigationTransitionHandler) + .startAnimation( + eq(transition), + argThat { info -> info.changes.contains(minimizingTaskChange) }, + any(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun startAnimation_withMinimizingLastDesktopTask_dispatchesTransition() { + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) + whenever( + desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any()) + ) + .thenReturn(true) + mixedHandler.addPendingMixedTransition( + PendingMixedTransition.Minimize( + transition = transition, + minimizingTask = minimizingTask.taskId, + isLastTask = true, + ) + ) + + val minimizingTaskChange = createChange(minimizingTask) + mixedHandler.startAnimation( + transition = transition, + info = + createTransitionInfo( + TRANSIT_TO_BACK, + listOf(minimizingTaskChange) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + verify(transitions) + .dispatchTransition( + eq(transition), + argThat { info -> info.changes.contains(minimizingTaskChange) }, + any(), + any(), + any(), + eq(mixedHandler) + ) + } + private fun createTransitionInfo( type: Int = WindowManager.TRANSIT_CLOSE, changeMode: Int = WindowManager.TRANSIT_CLOSE, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 414c1a658b95..7f790d574a7e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -936,6 +936,28 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test + fun saveBoundsBeforeMinimize_boundsSavedByTaskId() { + val taskId = 1 + val bounds = Rect(0, 0, 200, 200) + + repo.saveBoundsBeforeMinimize(taskId, bounds) + + assertThat(repo.removeBoundsBeforeMinimize(taskId)).isEqualTo(bounds) + } + + @Test + fun removeBoundsBeforeMinimize_returnsNullAfterBoundsRemoved() { + val taskId = 1 + val bounds = Rect(0, 0, 200, 200) + repo.saveBoundsBeforeMinimize(taskId, bounds) + repo.removeBoundsBeforeMinimize(taskId) + + val boundsBeforeMinimize = repo.removeBoundsBeforeMinimize(taskId) + + assertThat(boundsBeforeMinimize).isNull() + } + + @Test fun getExpandedTasksOrdered_returnsFreeformTasksInCorrectOrder() { repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 3, isVisible = true) repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 315a46fcbd7b..ad266ead774e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -3038,6 +3038,21 @@ class DesktopTasksControllerTest : ShellTestCase() { // Assert bounds set to stable bounds val wct = getLatestToggleResizeDesktopTaskWct() assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) + // Assert event is properly logged + verify(desktopModeEventLogger, times(1)).logTaskResizingStarted( + ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, + motionEvent, + task, + displayController + ) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, + motionEvent, + task, + STABLE_BOUNDS.height(), + STABLE_BOUNDS.width(), + displayController + ) } @Test @@ -3082,6 +3097,13 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(STABLE_BOUNDS), anyOrNull(), ) + // Assert no event is logged + verify(desktopModeEventLogger, never()).logTaskResizingStarted( + any(), any(), any(), any(), any() + ) + verify(desktopModeEventLogger, never()).logTaskResizingEnded( + any(), any(), any(), any(), any(), any(), any() + ) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 01b69aed8465..456b50da095b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect import android.os.Binder import android.os.Handler import android.platform.test.annotations.DisableFlags @@ -24,8 +25,10 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY +import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.TransitionInfo import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER @@ -63,6 +66,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.`when` import org.mockito.kotlin.eq @@ -235,6 +239,30 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test + fun onTransitionReady_pendingTransition_changeTaskToBack_boundsSaved() { + val bounds = Rect(0, 0, 200, 200) + val transition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + val change = TransitionInfo.Change(task.token, mock(SurfaceControl::class.java)).apply { + mode = TRANSIT_TO_BACK + taskInfo = task + setStartAbsBounds(bounds) + } + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) }, + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + assertThat(desktopTaskRepo.removeBoundsBeforeMinimize(taskId = task.taskId)).isEqualTo( + bounds) + } + + @Test fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() { val mergedTransition = Binder() val newTransition = Binder() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 737439ce3cfe..7f1c1db3207a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -76,6 +76,7 @@ class DesktopTasksTransitionObserverTest { private val context = mock<Context>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val taskRepository = mock<DesktopRepository>() + private val mixedHandler = mock<DesktopMixedTransitionHandler>() private lateinit var transitionObserver: DesktopTasksTransitionObserver private lateinit var shellInit: ShellInit @@ -87,7 +88,7 @@ class DesktopTasksTransitionObserverTest { transitionObserver = DesktopTasksTransitionObserver( - context, taskRepository, transitions, shellTaskOrganizer, shellInit + context, taskRepository, transitions, shellTaskOrganizer, mixedHandler, shellInit ) } @@ -106,6 +107,7 @@ class DesktopTasksTransitionObserverTest { ) verify(taskRepository).minimizeTask(task.displayId, task.taskId) + verify(mixedHandler).addPendingMixedTransition(any()) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java index 72950a8dc139..6d37ed766aef 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java @@ -28,8 +28,11 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import android.app.AppCompatTaskInfo; import android.app.TaskInfo; import android.graphics.Rect; import android.testing.AndroidTestingRunner; @@ -75,6 +78,7 @@ public class PipAnimationControllerTest extends ShellTestCase { .setContainerLayer() .setName("FakeLeash") .build(); + mTaskInfo.appCompatTaskInfo = mock(AppCompatTaskInfo.class); } @Test @@ -93,7 +97,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue1 = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); assertEquals("Expect ANIM_TYPE_BOUNDS animation", animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS); @@ -107,14 +112,16 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); oldAnimator.setSurfaceControlTransactionFactory( MockSurfaceControlHelper::createMockSurfaceControlTransaction); oldAnimator.start(); final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue2, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); assertEquals("getAnimator with same type returns same animator", oldAnimator, newAnimator); @@ -145,7 +152,8 @@ public class PipAnimationControllerTest extends ShellTestCase { // Fullscreen to PiP. PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null, - TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90); + TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90, + false /* alwaysAnimateTaskBounds */); // Apply fraction 1 to compute the end value. animator.applySurfaceControlTransaction(mLeash, tx, 1); final Rect rotatedEndBounds = new Rect(endBounds); @@ -157,7 +165,8 @@ public class PipAnimationControllerTest extends ShellTestCase { startBounds.set(0, 0, 1000, 500); endBounds.set(200, 100, 400, 500); animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, startBounds, startBounds, - endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270); + endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270, + false /* alwaysAnimateTaskBounds */); animator.applySurfaceControlTransaction(mLeash, tx, 1); rotatedEndBounds.set(endBounds); rotateBounds(rotatedEndBounds, startBounds, ROTATION_270); @@ -166,6 +175,37 @@ public class PipAnimationControllerTest extends ShellTestCase { } @Test + public void pipTransitionAnimator_rotatedEndValue_overrideMainWindowFrame() { + final SurfaceControl.Transaction tx = createMockSurfaceControlTransaction(); + final Rect startBounds = new Rect(200, 700, 400, 800); + final Rect endBounds = new Rect(0, 0, 500, 1000); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 250, 1000, 500); + + // Fullscreen task to PiP. + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null, + TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90, + false /* alwaysAnimateTaskBounds */); + // Apply fraction 1 to compute the end value. + animator.applySurfaceControlTransaction(mLeash, tx, 1); + + assertEquals("Expect use main window frame", mTaskInfo.topActivityMainWindowFrame, + animator.mCurrentValue); + + // PiP to fullscreen. + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 250, 1000, 500); + startBounds.set(0, 0, 1000, 500); + endBounds.set(200, 100, 400, 500); + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, startBounds, startBounds, + endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270, + false /* alwaysAnimateTaskBounds */); + animator.applySurfaceControlTransaction(mLeash, tx, 1); + + assertEquals("Expect use main window frame", mTaskInfo.topActivityMainWindowFrame, + animator.mCurrentValue); + } + + @Test @SuppressWarnings("unchecked") public void pipTransitionAnimator_updateEndValue() { final Rect baseValue = new Rect(0, 0, 100, 100); @@ -174,7 +214,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); animator.updateEndValue(endValue2); @@ -188,7 +229,8 @@ public class PipAnimationControllerTest extends ShellTestCase { final Rect endValue = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, - TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); animator.setSurfaceControlTransactionFactory( MockSurfaceControlHelper::createMockSurfaceControlTransaction); @@ -207,4 +249,126 @@ public class PipAnimationControllerTest extends ShellTestCase { verify(mPipAnimationCallback).onPipAnimationEnd(eq(mTaskInfo), any(SurfaceControl.Transaction.class), eq(animator)); } + + @Test + public void pipTransitionAnimator_overrideMainWindowFrame() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100); + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is overridden for in-PIP transition", + mTaskInfo.topActivityMainWindowFrame, animator.getBaseValue()); + assertEquals("Expect start value is overridden for in-PIP transition", + mTaskInfo.topActivityMainWindowFrame, animator.getStartValue()); + assertEquals("Expect end value is not overridden for in-PIP transition", + endValue, animator.getEndValue()); + + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue, + endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for leave-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for leave-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is overridden for leave-PIP transition", + mTaskInfo.topActivityMainWindowFrame, animator.getEndValue()); + } + + @Test + public void pipTransitionAnimator_animateTaskBounds() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100); + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + true /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for in-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for in-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for in-PIP transition", + endValue, animator.getEndValue()); + + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue, + endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0, + true /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for leave-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for leave-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for leave-PIP transition", + endValue, animator.getEndValue()); + } + + @Test + public void pipTransitionAnimator_letterboxed_animateTaskBounds() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100); + doReturn(true).when(mTaskInfo.appCompatTaskInfo).isTopActivityLetterboxed(); + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for in-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for in-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for in-PIP transition", + endValue, animator.getEndValue()); + + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue, + endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for leave-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for leave-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for leave-PIP transition", + endValue, animator.getEndValue()); + } + + @Test + public void pipTransitionAnimator_sizeCompat_animateTaskBounds() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue = new Rect(100, 100, 200, 200); + mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100); + doReturn(true).when(mTaskInfo.appCompatTaskInfo).isTopActivityInSizeCompat(); + PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null, + TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for in-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for in-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for in-PIP transition", + endValue, animator.getEndValue()); + + animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue, + endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0, + false /* alwaysAnimateTaskBounds */); + + assertEquals("Expect base value is not overridden for leave-PIP transition", + baseValue, animator.getBaseValue()); + assertEquals("Expect start value is not overridden for leave-PIP transition", + startValue, animator.getStartValue()); + assertEquals("Expect end value is not overridden for leave-PIP transition", + endValue, animator.getEndValue()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index bcb7461bfae7..5f58265b45f5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -47,6 +47,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; import com.android.wm.shell.MockSurfaceControlHelper; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; @@ -61,6 +62,7 @@ import com.android.wm.shell.common.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.common.pip.PipSnapAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.SizeSpecSource; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -90,6 +92,8 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; @Mock private PipUiEventLogger mMockPipUiEventLogger; @Mock private Optional<SplitScreenController> mMockOptionalSplitScreen; + @Mock private Optional<DesktopRepository> mMockOptionalDesktopRepository; + @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder; private TestShellExecutor mMainExecutor; @@ -120,8 +124,10 @@ public class PipTaskOrganizerTest extends ShellTestCase { mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, mMockPipTransitionController, mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, - Optional.empty() /* pipPerfHintControllerOptional */, mMockDisplayController, - mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor); + Optional.empty() /* pipPerfHintControllerOptional */, + mMockOptionalDesktopRepository, mRootTaskDisplayAreaOrganizer, + mMockDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer, + mMainExecutor); mMainExecutor.flushAll(); preparePipTaskOrg(); preparePipSurfaceTransactionHelper(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt index 5ebf5170bf86..59141ca39487 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration import android.content.ComponentName +import android.graphics.Region import android.testing.AndroidTestingRunner import android.view.Display import android.view.InsetsState @@ -33,6 +34,9 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidTestingRunner::class) class CaptionWindowDecorationTests : ShellTestCase() { + + private val exclusionRegion = Region.obtain() + @Test fun updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { val taskInfo = createTaskInfo() @@ -50,7 +54,8 @@ class CaptionWindowDecorationTests : ShellTestCase() { true /* isStatusBarVisible */, false /* isKeyguardVisibleAndOccluded */, InsetsState(), - true /* hasGlobalFocus */ + true /* hasGlobalFocus */, + exclusionRegion ) Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue() @@ -72,7 +77,8 @@ class CaptionWindowDecorationTests : ShellTestCase() { true /* isStatusBarVisible */, false /* isKeyguardVisibleAndOccluded */, InsetsState(), - true /* hasGlobalFocus */ + true /* hasGlobalFocus */, + exclusionRegion ) Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse() @@ -90,7 +96,8 @@ class CaptionWindowDecorationTests : ShellTestCase() { true /* isStatusBarVisible */, false /* isKeyguardVisibleAndOccluded */, InsetsState(), - true /* hasGlobalFocus */ + true /* hasGlobalFocus */, + exclusionRegion ) Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2) Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 956100d9bc03..be664f86e9f5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -30,6 +30,7 @@ import android.content.Intent import android.content.Intent.ACTION_MAIN import android.content.pm.ActivityInfo import android.graphics.Rect +import android.graphics.Region import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager @@ -48,6 +49,7 @@ import android.testing.TestableLooper.RunWithLooper import android.util.SparseArray import android.view.Choreographer import android.view.Display.DEFAULT_DISPLAY +import android.view.ISystemGestureExclusionListener import android.view.IWindowManager import android.view.InputChannel import android.view.InputMonitor @@ -84,7 +86,6 @@ 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.MultiInstanceHelper -import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler import com.android.wm.shell.desktopmode.DesktopModeEventLogger @@ -131,6 +132,7 @@ import org.mockito.Mockito.times import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.verify import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doNothing @@ -175,7 +177,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockInputMonitorFactory: DesktopModeWindowDecorViewModel.InputMonitorFactory @Mock private lateinit var mockShellController: ShellController - @Mock private lateinit var mockShellExecutor: ShellExecutor + private val testShellExecutor = TestShellExecutor() @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler @@ -230,13 +232,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { spyContext = spy(mContext) doNothing().`when`(spyContext).startActivity(any()) - shellInit = ShellInit(mockShellExecutor) + shellInit = ShellInit(testShellExecutor) windowDecorByTaskIdSpy.clear() spyContext.addMockSystemService(InputManager::class.java, mockInputManager) desktopModeEventLogger = mock<DesktopModeEventLogger>() desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( spyContext, - mockShellExecutor, + testShellExecutor, mockMainHandler, mockMainChoreographer, bgExecutor, @@ -1321,11 +1323,11 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { decoration.mHasGlobalFocus = true desktopModeWindowDecorViewModel.onTaskInfoChanged(task) - verify(decoration).relayout(task, true) + verify(decoration).relayout(eq(task), eq(true), anyOrNull()) decoration.mHasGlobalFocus = false desktopModeWindowDecorViewModel.onTaskInfoChanged(task) - verify(decoration).relayout(task, false) + verify(decoration).relayout(eq(task), eq(false), anyOrNull()) } @Test @@ -1342,17 +1344,66 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { task.isFocused = true desktopModeWindowDecorViewModel.onTaskInfoChanged(task) - verify(decoration).relayout(task, true) + verify(decoration).relayout(eq(task), eq(true), anyOrNull()) task.isFocused = false desktopModeWindowDecorViewModel.onTaskInfoChanged(task) - verify(decoration).relayout(task, false) + verify(decoration).relayout(eq(task), eq(false), anyOrNull()) + } + + @Test + fun testGestureExclusionChanged_updatesDecorations() { + val captor = argumentCaptor<ISystemGestureExclusionListener>() + verify(mockWindowManager) + .registerSystemGestureExclusionListener(captor.capture(), eq(DEFAULT_DISPLAY)) + val task = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + displayId = DEFAULT_DISPLAY + ) + val task2 = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + displayId = DEFAULT_DISPLAY + ) + val newRegion = Region.obtain().apply { + set(Rect(0, 0, 1600, 80)) + } + + captor.firstValue.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, newRegion, newRegion) + testShellExecutor.flushAll() + + verify(task).onExclusionRegionChanged(newRegion) + verify(task2).onExclusionRegionChanged(newRegion) + } + + @Test + fun testGestureExclusionChanged_otherDisplay_skipsDecorationUpdate() { + val captor = argumentCaptor<ISystemGestureExclusionListener>() + verify(mockWindowManager) + .registerSystemGestureExclusionListener(captor.capture(), eq(DEFAULT_DISPLAY)) + val task = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + displayId = DEFAULT_DISPLAY + ) + val task2 = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + displayId = 2 + ) + val newRegion = Region.obtain().apply { + set(Rect(0, 0, 1600, 80)) + } + + captor.firstValue.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, newRegion, newRegion) + testShellExecutor.flushAll() + + verify(task).onExclusionRegionChanged(newRegion) + verify(task2, never()).onExclusionRegionChanged(newRegion) } private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, taskSurface: SurfaceControl = SurfaceControl(), requestingImmersive: Boolean = false, + displayId: Int = DEFAULT_DISPLAY, onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, onImmersiveOrRestoreListenerCaptor: KArgumentCaptor<() -> Unit> = @@ -1376,6 +1427,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { ): DesktopModeWindowDecoration { val decor = setUpMockDecorationForTask(createTask( windowingMode = windowingMode, + displayId = displayId, requestingImmersive = requestingImmersive )) onTaskOpening(decor.mTaskInfo, taskSurface) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 41f57ae0fd97..1d2d0f078817 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -64,6 +64,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.Region; import android.net.Uri; import android.os.Handler; import android.os.SystemProperties; @@ -224,6 +225,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private TestableContext mTestableContext; private final ShellExecutor mBgExecutor = new TestShellExecutor(); private final AssistContent mAssistContent = new AssistContent(); + private final Region mExclusionRegion = Region.obtain(); /** Set up run before test class. */ @BeforeClass @@ -262,8 +264,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(), - anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), - anyInt(), anyInt())) + anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), + anyInt(), anyInt(), anyInt(), anyInt())) .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false); when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(), @@ -283,7 +285,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); - spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */, mExclusionRegion); // Menus should close if open before the task being invisible causes relayout to return. verify(spyWindowDecor).closeHandleMenu(); @@ -303,7 +305,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL); } @@ -324,7 +327,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mCornerRadius).isGreaterThan(0); } @@ -350,7 +354,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity); } @@ -377,12 +382,14 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity); } @Test + @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); @@ -400,12 +407,39 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.hasInputFeatureSpy()).isTrue(); } @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) + public void updateRelayoutParams_freeformAndTransparentAppearance_limitedTouchRegion() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance( + APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false, + /* isStatusBarVisible */ true, + /* isKeyguardVisibleAndOccluded */ false, + /* inFullImmersiveMode */ false, + new InsetsState(), + /* hasGlobalFocus= */ true, + mExclusionRegion); + + assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isTrue(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) public void updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); @@ -422,12 +456,38 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.hasInputFeatureSpy()).isFalse(); } @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) + public void updateRelayoutParams_freeformButOpaqueAppearance_unlimitedTouchRegion() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false, + /* isStatusBarVisible */ true, + /* isKeyguardVisibleAndOccluded */ false, + /* inFullImmersiveMode */ false, + new InsetsState(), + /* hasGlobalFocus= */ true, + mExclusionRegion); + + assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isFalse(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) public void updateRelayoutParams_fullscreen_disallowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); @@ -443,12 +503,36 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.hasInputFeatureSpy()).isFalse(); } @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS) + public void updateRelayoutParams_fullscreen_unlimitedTouchRegion() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false, + /* isStatusBarVisible */ true, + /* isKeyguardVisibleAndOccluded */ false, + /* inFullImmersiveMode */ false, + new InsetsState(), + /* hasGlobalFocus= */ true, + mExclusionRegion); + + assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isFalse(); + } + + @Test public void updateRelayoutParams_freeform_inputChannelNeeded() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); @@ -464,7 +548,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse(); } @@ -486,7 +571,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue(); } @@ -508,7 +594,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue(); } @@ -531,7 +618,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue(); } @@ -555,7 +643,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue(); } @@ -577,7 +666,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat( (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0) @@ -601,7 +691,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat( (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0) @@ -631,7 +722,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ true, insetsState, - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); // Takes status bar inset as padding, ignores caption bar inset. assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50); @@ -654,7 +746,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ true, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsInsetSource).isFalse(); } @@ -676,7 +769,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); // Header is always shown because it's assumed the status bar is always visible. assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -698,7 +792,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); } @@ -719,7 +814,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -740,7 +836,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ true, /* inFullImmersiveMode */ false, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -762,7 +859,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ true, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -776,7 +874,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ false, /* inFullImmersiveMode */ true, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -798,7 +897,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isKeyguardVisibleAndOccluded */ true, /* inFullImmersiveMode */ true, new InsetsState(), - /* hasGlobalFocus= */ true); + /* hasGlobalFocus= */ true, + mExclusionRegion); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -809,7 +909,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockTransaction).apply(); verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any()); @@ -824,7 +924,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) taskInfo.isResizeable = false; - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockTransaction, never()).apply(); verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction); @@ -836,7 +936,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any()); } @@ -848,7 +948,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); // Once for view host, the other for the AppHandle input layer. verify(mMockHandler, times(2)).post(runnableArgument.capture()); @@ -865,7 +965,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) taskInfo.isResizeable = false; - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); verify(mMockHandler, never()).post(any()); @@ -877,11 +977,11 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); // Once for view host, the other for the AppHandle input layer. verify(mMockHandler, times(2)).post(runnableArgument.capture()); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); } @@ -892,7 +992,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); // Once for view host, the other for the AppHandle input layer. verify(mMockHandler, times(2)).post(runnableArgument.capture()); @@ -1132,7 +1232,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { runnableArgument.getValue().run(); // Relayout decor with same captured link - decor.relayout(taskInfo, true /* hasGlobalFocus */); + decor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); // Verify handle menu's browser link not set to captured link since link is expired createHandleMenu(decor); @@ -1313,7 +1413,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any()); } @@ -1330,7 +1430,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( captionStateArgumentCaptor.capture()); @@ -1357,7 +1457,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout( runnableArgumentCaptor.capture()); runnableArgumentCaptor.getValue().invoke(); @@ -1380,7 +1480,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */, mExclusionRegion); verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( captionStateArgumentCaptor.capture()); @@ -1400,7 +1500,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); createHandleMenu(spyWindowDecor); verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( @@ -1425,7 +1525,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( CaptionState.class); - spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); createHandleMenu(spyWindowDecor); spyWindowDecor.closeHandleMenu(); @@ -1440,9 +1540,30 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void browserApp_webUriUsedForBrowserApp() { + // Make {@link AppToWebUtils#isBrowserApp} return true + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.handleAllWebDataURI = true; + resolveInfo.activityInfo = createActivityInfo(); + when(mMockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt())) + .thenReturn(List.of(resolveInfo)); + + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */, + TEST_URI3 /* generic link */); + + // Verify web uri used for browser applications + createHandleMenu(decor); + verifyHandleMenuCreated(TEST_URI2); + } + + private void verifyHandleMenuCreated(@Nullable Uri uri) { verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(), - any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), + any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)), anyInt(), anyInt(), anyInt(), anyInt()); } @@ -1522,7 +1643,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener); windowDecor.mDecorWindowContext = mContext; if (relayout) { - windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion); } return windowDecor; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index ade17c61eda1..7ec2cbf9460e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -242,7 +242,7 @@ class HandleMenuTest : ShellTestCase() { private fun createAndShowHandleMenu( splitPosition: Int? = null, - forceShowSystemBars: Boolean = false, + forceShowSystemBars: Boolean = false ): HandleMenu { val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) { R.layout.desktop_mode_app_header @@ -266,8 +266,9 @@ class HandleMenuTest : ShellTestCase() { WindowManagerWrapper(mockWindowManager), layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true, shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false, - shouldShowChangeAspectRatioButton = false, - null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50, + shouldShowChangeAspectRatioButton = false, isBrowserApp = false, + null /* openInAppOrBrowserIntent */, captionWidth = HANDLE_WIDTH, + captionHeight = 50, captionX = captionX, captionY = 0, ) @@ -278,7 +279,7 @@ class HandleMenuTest : ShellTestCase() { onNewWindowClickListener = mock(), onManageWindowsClickListener = mock(), onChangeAspectRatioClickListener = mock(), - openInBrowserClickListener = mock(), + openInAppOrBrowserClickListener = mock(), onOpenByDefaultClickListener = mock(), onCloseMenuClickListener = mock(), onOutsideTouchListener = mock(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 8e0434cb28f7..534803db5fe0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -60,6 +60,7 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; @@ -508,7 +509,7 @@ public class WindowDecorationTests extends ShellTestCase { final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */, - true /* hasGlobalFocus */); + true /* hasGlobalFocus */, Region.obtain()); verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT); } @@ -525,7 +526,7 @@ public class WindowDecorationTests extends ShellTestCase { mRelayoutParams.mCaptionTopPadding = 50; windowDecor.relayout(taskInfo, false /* applyStartTransactionOnDraw */, - true /* hasGlobalFocus */); + true /* hasGlobalFocus */, Region.obtain()); assertEquals(50, mRelayoutResult.mCaptionTopPadding); } @@ -944,7 +945,7 @@ public class WindowDecorationTests extends ShellTestCase { decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */)); - verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */); + verify(decor, times(2)).relayout(any(), any(), any(), any(), any(), any()); } @Test @@ -958,7 +959,7 @@ public class WindowDecorationTests extends ShellTestCase { decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */)); - verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */); + verify(decor, times(1)).relayout(any(), any(), any(), any(), any(), any()); } @Test @@ -973,7 +974,7 @@ public class WindowDecorationTests extends ShellTestCase { decor.onKeyguardStateChanged(true /* visible */, true /* occluding */); assertTrue(decor.mIsKeyguardVisibleAndOccluded); - verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */); + verify(decor, times(2)).relayout(any(), any(), any(), any(), any(), any()); } @Test @@ -987,7 +988,7 @@ public class WindowDecorationTests extends ShellTestCase { decor.onKeyguardStateChanged(false /* visible */, true /* occluding */); - verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */); + verify(decor, times(1)).relayout(any(), any(), any(), any(), any(), any()); } private ActivityManager.RunningTaskInfo createTaskInfo() { @@ -1061,9 +1062,16 @@ public class WindowDecorationTests extends ShellTestCase { surfaceControlViewHostFactory, desktopModeEventLogger); } - @Override void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) { - relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus); + relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus, + Region.obtain()); + } + + @Override + void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { + relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus, + displayExclusionRegion); } @Override @@ -1085,11 +1093,13 @@ public class WindowDecorationTests extends ShellTestCase { } void relayout(ActivityManager.RunningTaskInfo taskInfo, - boolean applyStartTransactionOnDraw, boolean hasGlobalFocus) { + boolean applyStartTransactionOnDraw, boolean hasGlobalFocus, + @NonNull Region displayExclusionRegion) { mRelayoutParams.mRunningTaskInfo = taskInfo; mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; mRelayoutParams.mLayoutResId = R.layout.caption_layout; mRelayoutParams.mHasGlobalFocus = hasGlobalFocus; + mRelayoutParams.mDisplayExclusionRegion.set(displayExclusionRegion); relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mMockView, mRelayoutResult); } diff --git a/libs/appfunctions/Android.bp b/libs/appfunctions/Android.bp index c6cee07d1946..5ab5a7a59c2a 100644 --- a/libs/appfunctions/Android.bp +++ b/libs/appfunctions/Android.bp @@ -18,10 +18,10 @@ package { } java_sdk_library { - name: "com.google.android.appfunctions.sidecar", + name: "com.android.extensions.appfunctions", owner: "google", srcs: ["java/**/*.java"], - api_packages: ["com.google.android.appfunctions.sidecar"], + api_packages: ["com.android.extensions.appfunctions"], dex_preopt: { enabled: false, }, @@ -31,9 +31,9 @@ java_sdk_library { } prebuilt_etc { - name: "appfunctions.sidecar.xml", + name: "appfunctions.extension.xml", system_ext_specific: true, sub_dir: "permissions", - src: "appfunctions.sidecar.xml", + src: "appfunctions.extension.xml", filename_from_src: true, } diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index faf84a8ab5ac..de402095e195 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -1,9 +1,29 @@ // Signature format: 2.0 -package com.google.android.appfunctions.sidecar { +package com.android.extensions.appfunctions { + + public final class AppFunctionException extends java.lang.Exception { + ctor public AppFunctionException(int, @Nullable String); + ctor public AppFunctionException(int, @Nullable String, @NonNull android.os.Bundle); + method public int getErrorCategory(); + method public int getErrorCode(); + method @Nullable public String getErrorMessage(); + method @NonNull public android.os.Bundle getExtras(); + field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8 + field public static final int ERROR_CANCELLED = 2001; // 0x7d1 + field public static final int ERROR_CATEGORY_APP = 3; // 0x3 + field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 + field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 + field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 + field public static final int ERROR_DENIED = 1000; // 0x3e8 + field public static final int ERROR_DISABLED = 1002; // 0x3ea + field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb + field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9 + field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0 + } public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); - method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); @@ -15,7 +35,7 @@ package com.google.android.appfunctions.sidecar { public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } @@ -29,33 +49,17 @@ package com.google.android.appfunctions.sidecar { public static final class ExecuteAppFunctionRequest.Builder { ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String); - method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest build(); - method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle); - method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument); + method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest build(); + method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle); + method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument); } public final class ExecuteAppFunctionResponse { - method public int getErrorCategory(); - method @Nullable public String getErrorMessage(); + ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument); + ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument, @NonNull android.os.Bundle); method @NonNull public android.os.Bundle getExtras(); - method public int getResultCode(); method @NonNull public android.app.appsearch.GenericDocument getResultDocument(); - method public boolean isSuccess(); - method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle); - method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle); - field public static final int ERROR_CATEGORY_APP = 3; // 0x3 - field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 - field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 - field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 - field public static final String PROPERTY_RETURN_VALUE = "returnValue"; - field public static final int RESULT_APP_UNKNOWN_ERROR = 3000; // 0xbb8 - field public static final int RESULT_CANCELLED = 2001; // 0x7d1 - field public static final int RESULT_DENIED = 1000; // 0x3e8 - field public static final int RESULT_DISABLED = 1002; // 0x3ea - field public static final int RESULT_FUNCTION_NOT_FOUND = 1003; // 0x3eb - field public static final int RESULT_INVALID_ARGUMENT = 1001; // 0x3e9 - field public static final int RESULT_OK = 0; // 0x0 - field public static final int RESULT_SYSTEM_ERROR = 2000; // 0x7d0 + field public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue"; } } diff --git a/libs/appfunctions/appfunctions.sidecar.xml b/libs/appfunctions/appfunctions.extension.xml index bef8b6ec7ce6..dd09cc39d12f 100644 --- a/libs/appfunctions/appfunctions.sidecar.xml +++ b/libs/appfunctions/appfunctions.extension.xml @@ -16,6 +16,6 @@ --> <permissions> <library - name="com.google.android.appfunctions.sidecar" - file="/system_ext/framework/com.google.android.appfunctions.sidecar.jar"/> + name="com.android.extensions.appfunctions" + file="/system_ext/framework/com.android.extensions.appfunctions.jar"/> </permissions>
\ No newline at end of file diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java new file mode 100644 index 000000000000..2540236f2ce5 --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.extensions.appfunctions; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Represents an app function related errors. */ +public final class AppFunctionException extends Exception { + /** + * The caller does not have the permission to execute an app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_DENIED = 1000; + + /** + * The caller supplied invalid arguments to the execution request. + * + * <p>This error may be considered similar to {@link IllegalArgumentException}. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_INVALID_ARGUMENT = 1001; + + /** + * The caller tried to execute a disabled app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_DISABLED = 1002; + + /** + * The caller tried to execute a function that does not exist. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_FUNCTION_NOT_FOUND = 1003; + + /** + * An internal unexpected error coming from the system. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int ERROR_SYSTEM_ERROR = 2000; + + /** + * The operation was cancelled. Use this error code to report that a cancellation is done after + * receiving a cancellation signal. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int ERROR_CANCELLED = 2001; + + /** + * An unknown error occurred while processing the call in the AppFunctionService. + * + * <p>This error is thrown when the service is connected in the remote application but an + * unexpected error is thrown from the bound application. + * + * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. + */ + public static final int ERROR_APP_UNKNOWN_ERROR = 3000; + + /** + * The error category is unknown. + * + * <p>This is the default value for {@link #getErrorCategory}. + */ + public static final int ERROR_CATEGORY_UNKNOWN = 0; + + /** + * The error is caused by the app requesting a function execution. + * + * <p>For example, the caller provided invalid parameters in the execution request e.g. an + * invalid function ID. + * + * <p>Errors in the category fall in the range 1000-1999 inclusive. + */ + public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; + + /** + * The error is caused by an issue in the system. + * + * <p>For example, the AppFunctionService implementation is not found by the system. + * + * <p>Errors in the category fall in the range 2000-2999 inclusive. + */ + public static final int ERROR_CATEGORY_SYSTEM = 2; + + /** + * The error is caused by the app providing the function. + * + * <p>For example, the app crashed when the system is executing the request. + * + * <p>Errors in the category fall in the range 3000-3999 inclusive. + */ + public static final int ERROR_CATEGORY_APP = 3; + + private final int mErrorCode; + @Nullable private final String mErrorMessage; + @NonNull private final Bundle mExtras; + + public AppFunctionException(int errorCode, @Nullable String errorMessage) { + this(errorCode, errorMessage, Bundle.EMPTY); + } + + public AppFunctionException( + int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) { + super(errorMessage); + mErrorCode = errorCode; + mErrorMessage = errorMessage; + mExtras = extras; + } + + /** Returns one of the {@code ERROR} constants. */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** Returns the error message. */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + /** + * Returns the error category. + * + * <p>This method categorizes errors based on their underlying cause, allowing developers to + * implement targeted error handling and provide more informative error messages to users. It + * maps ranges of error codes to specific error categories. + * + * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the error code does not belong to + * any error category. + * + * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding + * error code ranges. + */ + @ErrorCategory + public int getErrorCategory() { + if (mErrorCode >= 1000 && mErrorCode < 2000) { + return ERROR_CATEGORY_REQUEST_ERROR; + } + if (mErrorCode >= 2000 && mErrorCode < 3000) { + return ERROR_CATEGORY_SYSTEM; + } + if (mErrorCode >= 3000 && mErrorCode < 4000) { + return ERROR_CATEGORY_APP; + } + return ERROR_CATEGORY_UNKNOWN; + } + + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** + * Error codes. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_"}, + value = { + ERROR_DENIED, + ERROR_APP_UNKNOWN_ERROR, + ERROR_FUNCTION_NOT_FOUND, + ERROR_SYSTEM_ERROR, + ERROR_INVALID_ARGUMENT, + ERROR_DISABLED, + ERROR_CANCELLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCode {} + + /** + * Error categories. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_CATEGORY_"}, + value = { + ERROR_CATEGORY_UNKNOWN, + ERROR_CATEGORY_REQUEST_ERROR, + ERROR_CATEGORY_APP, + ERROR_CATEGORY_SYSTEM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCategory {} +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java index 2075104ff868..9eb66a33fedc 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar; +package com.android.extensions.appfunctions; import android.Manifest; import android.annotation.CallbackExecutor; @@ -31,7 +31,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; -import java.util.function.Consumer; /** * Provides app functions related functionalities. @@ -115,7 +114,9 @@ public final class AppFunctionManager { @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + @NonNull + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> + callback) { Objects.requireNonNull(sidecarRequest); Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -126,10 +127,20 @@ public final class AppFunctionManager { platformRequest, executor, cancellationSignal, - (platformResponse) -> { - callback.accept( - SidecarConverter.getSidecarExecuteAppFunctionResponse( - platformResponse)); + new OutcomeReceiver<>() { + @Override + public void onResult( + android.app.appfunctions.ExecuteAppFunctionResponse result) { + callback.onResult( + SidecarConverter.getSidecarExecuteAppFunctionResponse(result)); + } + + @Override + public void onError( + android.app.appfunctions.AppFunctionException exception) { + callback.onError( + SidecarConverter.getSidecarAppFunctionException(exception)); + } }); } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java index 0dc87e45b7e3..55f579138218 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar; +package com.android.extensions.appfunctions; -import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE; +import static com.android.extensions.appfunctions.SidecarConverter.getPlatformAppFunctionException; +import static com.android.extensions.appfunctions.SidecarConverter.getPlatformExecuteAppFunctionResponse; import android.annotation.MainThread; import android.annotation.NonNull; @@ -26,9 +27,7 @@ import android.content.Intent; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; -import android.util.Log; - -import java.util.function.Consumer; +import android.os.OutcomeReceiver; /** * Abstract base class to provide app functions to the system. @@ -80,10 +79,18 @@ public abstract class AppFunctionService extends Service { platformRequest), callingPackage, cancellationSignal, - (sidecarResponse) -> { - callback.accept( - SidecarConverter.getPlatformExecuteAppFunctionResponse( - sidecarResponse)); + new OutcomeReceiver<>() { + @Override + public void onResult(ExecuteAppFunctionResponse result) { + callback.onResult( + getPlatformExecuteAppFunctionResponse(result)); + } + + @Override + public void onError(AppFunctionException exception) { + callback.onError( + getPlatformAppFunctionException(exception)); + } }); }); @@ -116,12 +123,14 @@ public abstract class AppFunctionService extends Service { * @param request The function execution request. * @param callingPackage The package name of the app that is requesting the execution. * @param cancellationSignal A signal to cancel the execution. - * @param callback A callback to report back the result. + * @param callback A callback to report back the result or error. */ @MainThread public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback); + @NonNull + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> + callback); } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java index 593c5213dd52..baddc245f0f1 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar; +package com.android.extensions.appfunctions; import android.annotation.NonNull; import android.app.appsearch.GenericDocument; @@ -91,8 +91,8 @@ public final class ExecuteAppFunctionRequest { * Returns the function parameters. The key is the parameter name, and the value is the * parameter value. * - * <p>The bundle may have missing parameters. Developers are advised to implement defensive - * handling measures. + * <p>The {@link GenericDocument} may have missing parameters. Developers are advised to + * implement defensive handling measures. * * <p>Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java new file mode 100644 index 000000000000..0826f04a50dd --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.extensions.appfunctions; + +import android.annotation.NonNull; +import android.app.appfunctions.AppFunctionManager; +import android.app.appsearch.GenericDocument; +import android.os.Bundle; + +import java.util.Objects; + +/** The response to an app function execution. */ +public final class ExecuteAppFunctionResponse { + /** + * The name of the property that stores the function return value within the {@code + * resultDocument}. + * + * <p>See {@link GenericDocument#getProperty(String)} for more information. + * + * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will + * be empty {@link GenericDocument}. + * + * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will + * return {@code null}. + * + * <p>See {@link #getResultDocument} for more information on extracting the return value. + */ + public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue"; + + /** + * Returns the return value of the executed function. + * + * <p>The return value is stored in a {@link GenericDocument} with the key {@link + * #PROPERTY_RETURN_VALUE}. + * + * <p>See {@link #getResultDocument} for more information on extracting the return value. + */ + @NonNull private final GenericDocument mResultDocument; + + /** Returns the additional metadata data relevant to this function execution response. */ + @NonNull private final Bundle mExtras; + + /** + * @param resultDocument The return value of the executed function. + */ + public ExecuteAppFunctionResponse(@NonNull GenericDocument resultDocument) { + this(resultDocument, Bundle.EMPTY); + } + + /** + * @param resultDocument The return value of the executed function. + * @param extras The additional metadata for this function execution response. + */ + public ExecuteAppFunctionResponse( + @NonNull GenericDocument resultDocument, @NonNull Bundle extras) { + mResultDocument = Objects.requireNonNull(resultDocument); + mExtras = Objects.requireNonNull(extras); + } + + /** + * Returns a generic document containing the return value of the executed function. + * + * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. + * + * <p>Sample code for extracting the return value: + * + * <pre> + * GenericDocument resultDocument = response.getResultDocument(); + * Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE); + * if (returnValue != null) { + * // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString}, + * // {@link GenericDocument#getPropertyLong} etc. + * // Do something with the returnValue + * } + * </pre> + * + * @see AppFunctionManager on how to determine the expected function return. + */ + @NonNull + public GenericDocument getResultDocument() { + return mResultDocument; + } + + /** Returns the additional metadata for this function execution response. */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java index b1b05f79f33f..5e1fc7e684e2 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar; +package com.android.extensions.appfunctions; import android.annotation.NonNull; @@ -28,46 +28,50 @@ public final class SidecarConverter { private SidecarConverter() {} /** - * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest} - * into platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} + * Converts sidecar's {@link ExecuteAppFunctionRequest} into platform's {@link + * android.app.appfunctions.ExecuteAppFunctionRequest} * * @hide */ @NonNull public static android.app.appfunctions.ExecuteAppFunctionRequest getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) { - return new - android.app.appfunctions.ExecuteAppFunctionRequest.Builder( - request.getTargetPackageName(), - request.getFunctionIdentifier()) + return new android.app.appfunctions.ExecuteAppFunctionRequest.Builder( + request.getTargetPackageName(), request.getFunctionIdentifier()) .setExtras(request.getExtras()) .setParameters(request.getParameters()) .build(); } /** - * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse} - * into platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} + * Converts sidecar's {@link ExecuteAppFunctionResponse} into platform's {@link + * android.app.appfunctions.ExecuteAppFunctionResponse} * * @hide */ @NonNull public static android.app.appfunctions.ExecuteAppFunctionResponse getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) { - if (response.isSuccess()) { - return android.app.appfunctions.ExecuteAppFunctionResponse.newSuccess( - response.getResultDocument(), response.getExtras()); - } else { - return android.app.appfunctions.ExecuteAppFunctionResponse.newFailure( - response.getResultCode(), - response.getErrorMessage(), - response.getExtras()); - } + return new android.app.appfunctions.ExecuteAppFunctionResponse( + response.getResultDocument(), response.getExtras()); } /** - * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} - * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest} + * Converts sidecar's {@link AppFunctionException} into platform's {@link + * android.app.appfunctions.AppFunctionException} + * + * @hide + */ + @NonNull + public static android.app.appfunctions.AppFunctionException + getPlatformAppFunctionException(@NonNull AppFunctionException exception) { + return new android.app.appfunctions.AppFunctionException( + exception.getErrorCode(), exception.getErrorMessage(), exception.getExtras()); + } + + /** + * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} into sidecar's + * {@link ExecuteAppFunctionRequest} * * @hide */ @@ -75,30 +79,34 @@ public final class SidecarConverter { public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest( @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) { return new ExecuteAppFunctionRequest.Builder( - request.getTargetPackageName(), - request.getFunctionIdentifier()) + request.getTargetPackageName(), request.getFunctionIdentifier()) .setExtras(request.getExtras()) .setParameters(request.getParameters()) .build(); } /** - * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} - * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse} + * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} into + * sidecar's {@link ExecuteAppFunctionResponse} * * @hide */ @NonNull public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse( @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) { - if (response.isSuccess()) { - return ExecuteAppFunctionResponse.newSuccess( - response.getResultDocument(), response.getExtras()); - } else { - return ExecuteAppFunctionResponse.newFailure( - response.getResultCode(), - response.getErrorMessage(), - response.getExtras()); - } + return new ExecuteAppFunctionResponse(response.getResultDocument(), response.getExtras()); + } + + /** + * Converts platform's {@link android.app.appfunctions.AppFunctionException} into + * sidecar's {@link AppFunctionException} + * + * @hide + */ + @NonNull + public static AppFunctionException getSidecarAppFunctionException( + @NonNull android.app.appfunctions.AppFunctionException exception) { + return new AppFunctionException( + exception.getErrorCode(), exception.getErrorMessage(), exception.getExtras()); } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java deleted file mode 100644 index 4e88fb025a9d..000000000000 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.appfunctions.sidecar; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.appsearch.GenericDocument; -import android.os.Bundle; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Objects; - -/** - * The response to an app function execution. - * - * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionResponse} without parcel - * functionality and exposes it here as a sidecar library (avoiding direct dependency on the - * platform API). - */ -public final class ExecuteAppFunctionResponse { - /** - * The name of the property that stores the function return value within the {@code - * resultDocument}. - * - * <p>See {@link GenericDocument#getProperty(String)} for more information. - * - * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will - * be empty {@link GenericDocument}. - * - * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will - * return {@code null}. - * - * <p>See {@link #getResultDocument} for more information on extracting the return value. - */ - public static final String PROPERTY_RETURN_VALUE = "returnValue"; - - /** - * The call was successful. - * - * <p>This result code does not belong in an error category. - */ - public static final int RESULT_OK = 0; - - /** - * The caller does not have the permission to execute an app function. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_DENIED = 1000; - - /** - * The caller supplied invalid arguments to the execution request. - * - * <p>This error may be considered similar to {@link IllegalArgumentException}. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_INVALID_ARGUMENT = 1001; - - /** - * The caller tried to execute a disabled app function. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_DISABLED = 1002; - - /** - * The caller tried to execute a function that does not exist. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_FUNCTION_NOT_FOUND = 1003; - - /** - * An internal unexpected error coming from the system. - * - * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. - */ - public static final int RESULT_SYSTEM_ERROR = 2000; - - /** - * The operation was cancelled. Use this error code to report that a cancellation is done after - * receiving a cancellation signal. - * - * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. - */ - public static final int RESULT_CANCELLED = 2001; - - /** - * An unknown error occurred while processing the call in the AppFunctionService. - * - * <p>This error is thrown when the service is connected in the remote application but an - * unexpected error is thrown from the bound application. - * - * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. - */ - public static final int RESULT_APP_UNKNOWN_ERROR = 3000; - - /** - * The error category is unknown. - * - * <p>This is the default value for {@link #getErrorCategory}. - */ - public static final int ERROR_CATEGORY_UNKNOWN = 0; - - /** - * The error is caused by the app requesting a function execution. - * - * <p>For example, the caller provided invalid parameters in the execution request e.g. an - * invalid function ID. - * - * <p>Errors in the category fall in the range 1000-1999 inclusive. - */ - public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; - - /** - * The error is caused by an issue in the system. - * - * <p>For example, the AppFunctionService implementation is not found by the system. - * - * <p>Errors in the category fall in the range 2000-2999 inclusive. - */ - public static final int ERROR_CATEGORY_SYSTEM = 2; - - /** - * The error is caused by the app providing the function. - * - * <p>For example, the app crashed when the system is executing the request. - * - * <p>Errors in the category fall in the range 3000-3999 inclusive. - */ - public static final int ERROR_CATEGORY_APP = 3; - - /** The result code of the app function execution. */ - @ResultCode private final int mResultCode; - - /** - * The error message associated with the result, if any. This is {@code null} if the result code - * is {@link #RESULT_OK}. - */ - @Nullable private final String mErrorMessage; - - /** - * Returns the return value of the executed function. - * - * <p>The return value is stored in a {@link GenericDocument} with the key {@link - * #PROPERTY_RETURN_VALUE}. - * - * <p>See {@link #getResultDocument} for more information on extracting the return value. - */ - @NonNull private final GenericDocument mResultDocument; - - /** Returns the additional metadata data relevant to this function execution response. */ - @NonNull private final Bundle mExtras; - - private ExecuteAppFunctionResponse( - @NonNull GenericDocument resultDocument, - @NonNull Bundle extras, - @ResultCode int resultCode, - @Nullable String errorMessage) { - mResultDocument = Objects.requireNonNull(resultDocument); - mExtras = Objects.requireNonNull(extras); - mResultCode = resultCode; - mErrorMessage = errorMessage; - } - - /** - * Returns result codes from throwable. - * - * @hide - */ - static @ResultCode int getResultCode(@NonNull Throwable t) { - if (t instanceof IllegalArgumentException) { - return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT; - } - return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR; - } - - /** - * Returns a successful response. - * - * @param resultDocument The return value of the executed function. - * @param extras The additional metadata data relevant to this function execution response. - */ - @NonNull - public static ExecuteAppFunctionResponse newSuccess( - @NonNull GenericDocument resultDocument, @Nullable Bundle extras) { - Objects.requireNonNull(resultDocument); - Bundle actualExtras = getActualExtras(extras); - - return new ExecuteAppFunctionResponse( - resultDocument, actualExtras, RESULT_OK, /* errorMessage= */ null); - } - - /** - * Returns a failure response. - * - * @param resultCode The result code of the app function execution. - * @param extras The additional metadata data relevant to this function execution response. - * @param errorMessage The error message associated with the result, if any. - */ - @NonNull - public static ExecuteAppFunctionResponse newFailure( - @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) { - if (resultCode == RESULT_OK) { - throw new IllegalArgumentException("resultCode must not be RESULT_OK"); - } - Bundle actualExtras = getActualExtras(extras); - GenericDocument emptyDocument = new GenericDocument.Builder<>("", "", "").build(); - return new ExecuteAppFunctionResponse( - emptyDocument, actualExtras, resultCode, errorMessage); - } - - private static Bundle getActualExtras(@Nullable Bundle extras) { - if (extras == null) { - return Bundle.EMPTY; - } - return extras; - } - - /** - * Returns the error category of the {@link ExecuteAppFunctionResponse}. - * - * <p>This method categorizes errors based on their underlying cause, allowing developers to - * implement targeted error handling and provide more informative error messages to users. It - * maps ranges of result codes to specific error categories. - * - * <p>When constructing a {@link #newFailure} response, use the appropriate result code value to - * ensure correct categorization of the failed response. - * - * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to - * any error category, for example, in the case of a successful result with {@link #RESULT_OK}. - * - * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding - * result code ranges. - */ - @ErrorCategory - public int getErrorCategory() { - if (mResultCode >= 1000 && mResultCode < 2000) { - return ERROR_CATEGORY_REQUEST_ERROR; - } - if (mResultCode >= 2000 && mResultCode < 3000) { - return ERROR_CATEGORY_SYSTEM; - } - if (mResultCode >= 3000 && mResultCode < 4000) { - return ERROR_CATEGORY_APP; - } - return ERROR_CATEGORY_UNKNOWN; - } - - /** - * Returns a generic document containing the return value of the executed function. - * - * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. - * - * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed - * function does not produce a return value. - * - * <p>Sample code for extracting the return value: - * - * <pre> - * GenericDocument resultDocument = response.getResultDocument(); - * Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE); - * if (returnValue != null) { - * // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString}, - * // {@link GenericDocument#getPropertyLong} etc. - * // Do something with the returnValue - * } - * </pre> - */ - @NonNull - public GenericDocument getResultDocument() { - return mResultDocument; - } - - /** Returns the extras of the app function execution. */ - @NonNull - public Bundle getExtras() { - return mExtras; - } - - /** - * Returns {@code true} if {@link #getResultCode} equals {@link - * ExecuteAppFunctionResponse#RESULT_OK}. - */ - public boolean isSuccess() { - return getResultCode() == RESULT_OK; - } - - /** - * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}. - */ - @ResultCode - public int getResultCode() { - return mResultCode; - } - - /** - * Returns the error message associated with this result. - * - * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. - */ - @Nullable - public String getErrorMessage() { - return mErrorMessage; - } - - /** - * Result codes. - * - * @hide - */ - @IntDef( - prefix = {"RESULT_"}, - value = { - RESULT_OK, - RESULT_DENIED, - RESULT_APP_UNKNOWN_ERROR, - RESULT_SYSTEM_ERROR, - RESULT_FUNCTION_NOT_FOUND, - RESULT_INVALID_ARGUMENT, - RESULT_DISABLED, - RESULT_CANCELLED - }) - @Retention(RetentionPolicy.SOURCE) - public @interface ResultCode {} - - /** - * Error categories. - * - * @hide - */ - @IntDef( - prefix = {"ERROR_CATEGORY_"}, - value = { - ERROR_CATEGORY_UNKNOWN, - ERROR_CATEGORY_REQUEST_ERROR, - ERROR_CATEGORY_APP, - ERROR_CATEGORY_SYSTEM - }) - @Retention(RetentionPolicy.SOURCE) - public @interface ErrorCategory {} -} diff --git a/libs/appfunctions/tests/Android.bp b/libs/appfunctions/tests/Android.bp index 6f5eff305d8d..db79675ae9f7 100644 --- a/libs/appfunctions/tests/Android.bp +++ b/libs/appfunctions/tests/Android.bp @@ -25,7 +25,7 @@ android_test { "androidx.test.rules", "androidx.test.ext.junit", "androidx.core_core-ktx", - "com.google.android.appfunctions.sidecar.impl", + "com.android.extensions.appfunctions.impl", "junit", "kotlin-test", "mockito-target-extended-minus-junit4", diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/android/extensions/appfunctions/tests/SidecarConverterTest.kt index 264f84209caf..11202d58e484 100644 --- a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt +++ b/libs/appfunctions/tests/src/com/android/extensions/appfunctions/tests/SidecarConverterTest.kt @@ -14,14 +14,15 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar.tests +package com.android.extensions.appfunctions.tests +import android.app.appfunctions.AppFunctionException import android.app.appfunctions.ExecuteAppFunctionRequest import android.app.appfunctions.ExecuteAppFunctionResponse import android.app.appsearch.GenericDocument import android.os.Bundle import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.android.appfunctions.sidecar.SidecarConverter +import com.android.extensions.appfunctions.SidecarConverter import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -60,7 +61,7 @@ class SidecarConverterTest { .setPropertyLong("testLong", 23) .build() val sidecarRequest = - com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder( + com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder( "targetPkg", "targetFunctionId" ) @@ -83,44 +84,38 @@ class SidecarConverterTest { GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "") .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true) .build() - val platformResponse = ExecuteAppFunctionResponse.newSuccess(resultGd, null) + val platformResponse = ExecuteAppFunctionResponse(resultGd) val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse( platformResponse ) - assertThat(sidecarResponse.isSuccess).isTrue() assertThat( sidecarResponse.resultDocument.getProperty( ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE ) ) .isEqualTo(booleanArrayOf(true)) - assertThat(sidecarResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK) - assertThat(sidecarResponse.errorMessage).isNull() } @Test - fun getSidecarExecuteAppFunctionResponse_errorResponse_sameContents() { - val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build() - val platformResponse = - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, - null, - null + fun getSidecarAppFunctionException_sameContents() { + val bundle = Bundle() + bundle.putString("key", "value") + val platformException = + AppFunctionException( + AppFunctionException.ERROR_SYSTEM_ERROR, + "error", + bundle ) - val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse( - platformResponse + val sidecarException = SidecarConverter.getSidecarAppFunctionException( + platformException ) - assertThat(sidecarResponse.isSuccess).isFalse() - assertThat(sidecarResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace) - assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id) - assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType) - assertThat(sidecarResponse.resultCode) - .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR) - assertThat(sidecarResponse.errorMessage).isNull() + assertThat(sidecarException.errorCode).isEqualTo(AppFunctionException.ERROR_SYSTEM_ERROR) + assertThat(sidecarException.errorMessage).isEqualTo("error") + assertThat(sidecarException.extras.getString("key")).isEqualTo("value") } @Test @@ -129,44 +124,39 @@ class SidecarConverterTest { GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "") .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true) .build() - val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse - .newSuccess(resultGd, null) + val sidecarResponse = + com.android.extensions.appfunctions.ExecuteAppFunctionResponse(resultGd) val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse( sidecarResponse ) - assertThat(platformResponse.isSuccess).isTrue() assertThat( platformResponse.resultDocument.getProperty( ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE ) ) .isEqualTo(booleanArrayOf(true)) - assertThat(platformResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK) - assertThat(platformResponse.errorMessage).isNull() } @Test - fun getPlatformExecuteAppFunctionResponse_errorResponse_sameContents() { - val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build() - val sidecarResponse = - com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, - null, - null + fun getPlatformAppFunctionException_sameContents() { + val bundle = Bundle() + bundle.putString("key", "value") + val sidecarException = + com.android.extensions.appfunctions.AppFunctionException( + AppFunctionException.ERROR_SYSTEM_ERROR, + "error", + bundle ) - val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse( - sidecarResponse + val platformException = SidecarConverter.getPlatformAppFunctionException( + sidecarException ) - assertThat(platformResponse.isSuccess).isFalse() - assertThat(platformResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace) - assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id) - assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType) - assertThat(platformResponse.resultCode) - .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR) - assertThat(platformResponse.errorMessage).isNull() + assertThat(platformException.errorCode) + .isEqualTo(AppFunctionException.ERROR_SYSTEM_ERROR) + assertThat(platformException.errorMessage).isEqualTo("error") + assertThat(platformException.extras.getString("key")).isEqualTo("value") } } diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h index fddcf29b9197..5f84f47b725d 100644 --- a/libs/hwui/FeatureFlags.h +++ b/libs/hwui/FeatureFlags.h @@ -33,9 +33,9 @@ inline bool letter_spacing_justification() { #endif // __ANDROID__ } -inline bool typeface_redesign() { +inline bool typeface_redesign_readonly() { #ifdef __ANDROID__ - static bool flag = com_android_text_flags_typeface_redesign(); + static bool flag = com_android_text_flags_typeface_redesign_readonly(); return flag; #else return true; diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 5ad788c67816..fa27af671be6 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -154,3 +154,13 @@ flag { description: "API's that enable animated image drawables to use nearest sampling when scaling." bug: "370523334" } + +flag { + name: "remove_vri_sketchy_destroy" + namespace: "core_graphics" + description: "Remove the eager yet thread-violating destroyHardwareResources in VRI#die" + bug: "377057106" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h index 1510ce1378d8..20acf981d9b9 100644 --- a/libs/hwui/hwui/MinikinUtils.h +++ b/libs/hwui/hwui/MinikinUtils.h @@ -73,7 +73,7 @@ public: static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) { float saveSkewX = paint->getSkFont().getSkewX(); bool savefakeBold = paint->getSkFont().isEmbolden(); - if (text_feature::typeface_redesign()) { + if (text_feature::typeface_redesign_readonly()) { for (uint32_t runIdx = 0; runIdx < layout.getFontRunCount(); ++runIdx) { uint32_t start = layout.getFontRunStart(runIdx); uint32_t end = layout.getFontRunEnd(runIdx); diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp index 70e6beda6cb9..5f693462af91 100644 --- a/libs/hwui/jni/text/TextShaper.cpp +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -86,7 +86,7 @@ static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int cou overallDescent = std::max(overallDescent, extent.descent); } - if (text_feature::typeface_redesign()) { + if (text_feature::typeface_redesign_readonly()) { uint32_t runCount = layout.getFontRunCount(); std::unordered_map<minikin::FakedFont, uint32_t, FakedFontKey> fakedToFontIds; @@ -229,7 +229,7 @@ float findValueFromVariationSettings(const minikin::FontFakery& fakery, minikin: // CriticalNative static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); - if (text_feature::typeface_redesign()) { + if (text_feature::typeface_redesign_readonly()) { float value = findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_wght); return std::isnan(value) ? NO_OVERRIDE : value; @@ -241,7 +241,7 @@ static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlon // CriticalNative static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); - if (text_feature::typeface_redesign()) { + if (text_feature::typeface_redesign_readonly()) { float value = findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_ital); return std::isnan(value) ? NO_OVERRIDE : value; diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index 88efed55c11f..3f9126aa9456 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -1000,7 +1000,10 @@ public final class MediaCas implements AutoCloseable { @SystemApi @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) public boolean updateResourcePriority(int priority, int niceValue) { - return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue); + if (mTunerResourceManager != null) { + return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue); + } + return false; } /** @@ -1017,7 +1020,9 @@ public final class MediaCas implements AutoCloseable { @SystemApi @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) public void setResourceHolderRetain(boolean resourceHolderRetain) { - mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain); + if (mTunerResourceManager != null) { + mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain); + } } IHwBinder getBinder() { diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index e575daeb8d29..2ae89d3300c1 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -18,6 +18,7 @@ package android.media; import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE; import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST; +import static android.media.codec.Flags.FLAG_SUBSESSION_METRICS; import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME; @@ -890,7 +891,7 @@ import java.util.function.Supplier; any start codes), and submit it as a <strong>regular</strong> input buffer. <p> You will receive an {@link #INFO_OUTPUT_FORMAT_CHANGED} return value from {@link - #dequeueOutputBuffer dequeueOutputBuffer} or a {@link Callback#onOutputBufferAvailable + #dequeueOutputBuffer dequeueOutputBuffer} or a {@link Callback#onOutputFormatChanged onOutputFormatChanged} callback just after the picture-size change takes place and before any frames with the new size have been returned. <p class=note> @@ -1835,6 +1836,13 @@ final public class MediaCodec { private static final int CB_CRYPTO_ERROR = 6; private static final int CB_LARGE_FRAME_OUTPUT_AVAILABLE = 7; + /** + * Callback ID for when the metrics for this codec have been flushed due to + * the start of a new subsession. The associated Java Message object will + * contain the flushed metrics as a PersistentBundle in the obj field. + */ + private static final int CB_METRICS_FLUSHED = 8; + private class EventHandler extends Handler { private MediaCodec mCodec; @@ -2007,6 +2015,15 @@ final public class MediaCodec { break; } + case CB_METRICS_FLUSHED: + { + + if (GetFlag(() -> android.media.codec.Flags.subsessionMetrics())) { + mCallback.onMetricsFlushed(mCodec, (PersistableBundle)msg.obj); + } + break; + } + default: { break; @@ -4958,14 +4975,24 @@ final public class MediaCodec { public native final String getCanonicalName(); /** - * Return Metrics data about the current codec instance. + * Return Metrics data about the current codec instance. + * <p> + * Call this method after configuration, during execution, or after + * the codec has been already stopped. + * <p> + * Beginning with {@link android.os.Build.VERSION_CODES#B} + * this method can be used to get the Metrics data prior to an error. + * (e.g. in {@link Callback#onError} or after a method throws + * {@link MediaCodec.CodecException}.) Before that, the Metrics data was + * cleared on error, resulting in a null return value. * * @return a {@link PersistableBundle} containing the set of attributes and values * available for the media being handled by this instance of MediaCodec * The attributes are descibed in {@link MetricsConstants}. * * Additional vendor-specific fields may also be present in - * the return value. + * the return value. Returns null if there is no Metrics data. + * */ public PersistableBundle getMetrics() { PersistableBundle bundle = native_getMetrics(); @@ -5692,6 +5719,27 @@ final public class MediaCodec { */ public abstract void onOutputFormatChanged( @NonNull MediaCodec codec, @NonNull MediaFormat format); + + /** + * Called when the metrics for this codec have been flushed due to the + * start of a new subsession. + * <p> + * This can happen when the codec is reconfigured after stop(), or + * mid-stream e.g. if the video size changes. When this happens, the + * metrics for the previous subsession are flushed, and + * {@link MediaCodec#getMetrics} will return the metrics for the + * new subsession. This happens just before the {@link Callback#onOutputFormatChanged} + * event, so this <b>optional</b> callback is provided to be able to + * capture the final metrics for the previous subsession. + * + * @param codec The MediaCodec object. + * @param metrics The flushed metrics for this codec. + */ + @FlaggedApi(FLAG_SUBSESSION_METRICS) + public void onMetricsFlushed( + @NonNull MediaCodec codec, @NonNull PersistableBundle metrics) { + // default implementation ignores this callback. + } } private void postEventFromNative( diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3499c438086d..20108e7369d7 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -1771,10 +1771,12 @@ public final class MediaRouter2 { } /** - * A class to control media routing session in media route provider. For example, - * selecting/deselecting/transferring to routes of a session can be done through this. Instances - * are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is - * called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called. + * Controls a media routing session. + * + * <p>Routing controllers wrap a {@link RoutingSessionInfo}, taking care of mapping route ids to + * {@link MediaRoute2Info} instances. You can still access the underlying session using {@link + * #getRoutingSessionInfo()}, but keep in mind it can be changed by other threads. Changes to + * the routing session are notified via {@link ControllerCallback}. */ public class RoutingController { private final Object mControllerLock = new Object(); @@ -1836,7 +1838,9 @@ public final class MediaRouter2 { } /** - * @return the unmodifiable list of currently selected routes + * Returns the unmodifiable list of currently selected routes + * + * @see RoutingSessionInfo#getSelectedRoutes() */ @NonNull public List<MediaRoute2Info> getSelectedRoutes() { @@ -1848,7 +1852,9 @@ public final class MediaRouter2 { } /** - * @return the unmodifiable list of selectable routes for the session. + * Returns the unmodifiable list of selectable routes for the session. + * + * @see RoutingSessionInfo#getSelectableRoutes() */ @NonNull public List<MediaRoute2Info> getSelectableRoutes() { @@ -1860,7 +1866,9 @@ public final class MediaRouter2 { } /** - * @return the unmodifiable list of deselectable routes for the session. + * Returns the unmodifiable list of deselectable routes for the session. + * + * @see RoutingSessionInfo#getDeselectableRoutes() */ @NonNull public List<MediaRoute2Info> getDeselectableRoutes() { diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index 83a4dd5a682a..3b8cf3fb2909 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -262,7 +262,8 @@ public final class RoutingSessionInfo implements Parcelable { } /** - * Gets the provider id of the session. + * Gets the provider ID of the session. + * * @hide */ @Nullable @@ -271,7 +272,15 @@ public final class RoutingSessionInfo implements Parcelable { } /** - * Gets the list of IDs of selected routes for the session. It shouldn't be empty. + * Gets the list of IDs of selected routes for the session. + * + * <p>Selected routes are the routes that this session is actively routing media to. + * + * <p>The behavior of a routing session with multiple selected routes is ultimately defined by + * the {@link MediaRoute2ProviderService} implementation. However, typically, it's expected that + * all the selected routes of a routing session are playing the same media in sync. + * + * @return A non-empty list of selected route ids. */ @NonNull public List<String> getSelectedRoutes() { @@ -280,6 +289,16 @@ public final class RoutingSessionInfo implements Parcelable { /** * Gets the list of IDs of selectable routes for the session. + * + * <p>Selectable routes can be added to a routing session (via {@link + * MediaRouter2.RoutingController#selectRoute}) in order to add them to the {@link + * #getSelectedRoutes() selected routes}, so that media plays on the newly selected route along + * with the other selected routes. + * + * <p>Not to be confused with {@link #getTransferableRoutes() transferable routes}. Transferring + * to a route makes it the sole selected route. + * + * @return A possibly empty list of selectable route ids. */ @NonNull public List<String> getSelectableRoutes() { @@ -288,6 +307,17 @@ public final class RoutingSessionInfo implements Parcelable { /** * Gets the list of IDs of deselectable routes for the session. + * + * <p>Deselectable routes can be removed from the {@link #getSelectedRoutes() selected routes}, + * so that the routing session stops routing to the newly deselected route, but continues on any + * remaining selected routes. + * + * <p>Deselectable routes should be a subset of the {@link #getSelectedRoutes() selected + * routes}, meaning not all of the selected routes might be deselectable. For example, one of + * the selected routes may be a leader device coordinating group playback, which must always + * remain selected while the session is active. + * + * @return A possibly empty list of deselectable route ids. */ @NonNull public List<String> getDeselectableRoutes() { @@ -296,6 +326,24 @@ public final class RoutingSessionInfo implements Parcelable { /** * Gets the list of IDs of transferable routes for the session. + * + * <p>Transferring to a route (for example, using {@link MediaRouter2#transferTo}) replaces the + * list of {@link #getSelectedRoutes() selected routes} with the target route, causing playback + * to move from one route to another. + * + * <p>Note that this is different from {@link #getSelectableRoutes() selectable routes}, because + * selecting a route makes it part of the selected routes, while transferring to a route makes + * it the selected route. A route can be both transferable and selectable. + * + * <p>Note that playback may transfer across routes without the target route being in the list + * of transferable routes. This can happen by creating a new routing session to the target + * route, and releasing the routing session being transferred from. The difference is that a + * transfer to a route in the transferable list can happen with no intervention from the app, + * with the route provider taking care of the entire operation. A transfer to a route that is + * not in the list of transferable routes (by creating a new session) requires the app to move + * the playback state from one device to the other. + * + * @return A possibly empty list of transferable route ids. */ @NonNull public List<String> getTransferableRoutes() { diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 52a21e241ba8..7895eb27b372 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -51,7 +51,7 @@ flag { is_exported: true namespace: "media_tv" description: "Enables the following type constant in MediaRoute2Info: LINE_ANALOG, LINE_DIGITAL, AUX_LINE" - bug: "301713440" + bug: "375691732" } flag { @@ -166,3 +166,10 @@ flag { description: "Allows audio input devices routing and volume control via system settings." bug: "355684672" } + +flag { + name: "enable_mirroring_in_media_router_2" + namespace: "media_better_together" + description: "Enables support for mirroring routes in the MediaRouter2 framework, allowing Output Switcher to offer mirroring routes." + bug: "362507305" +} diff --git a/media/java/android/media/quality/AmbientBacklightSettings.java b/media/java/android/media/quality/AmbientBacklightSettings.java index 391eb225bcab..bb782bf1aee4 100644 --- a/media/java/android/media/quality/AmbientBacklightSettings.java +++ b/media/java/android/media/quality/AmbientBacklightSettings.java @@ -26,6 +26,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** + * Settings for ambient backlight. * @hide */ public class AmbientBacklightSettings implements Parcelable { diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/IMediaQualityManager.aidl index e6c79dd9681f..250d59b7c2d7 100644 --- a/media/java/android/media/quality/IMediaQualityManager.aidl +++ b/media/java/android/media/quality/IMediaQualityManager.aidl @@ -30,20 +30,22 @@ import android.media.quality.SoundProfile; */ interface IMediaQualityManager { PictureProfile createPictureProfile(in PictureProfile pp); - void updatePictureProfile(in long id, in PictureProfile pp); - void removePictureProfile(in long id); - PictureProfile getPictureProfileById(in long id); + void updatePictureProfile(in String id, in PictureProfile pp); + void removePictureProfile(in String id); + PictureProfile getPictureProfile(in int type, in String name); List<PictureProfile> getPictureProfilesByPackage(in String packageName); List<PictureProfile> getAvailablePictureProfiles(); - List<PictureProfile> getAllPictureProfiles(); + List<String> getPictureProfilePackageNames(); + List<String> getPictureProfileAllowList(); + void setPictureProfileAllowList(in List<String> packages); SoundProfile createSoundProfile(in SoundProfile pp); - void updateSoundProfile(in long id, in SoundProfile pp); - void removeSoundProfile(in long id); - SoundProfile getSoundProfileById(in long id); + void updateSoundProfile(in String id, in SoundProfile pp); + void removeSoundProfile(in String id); + SoundProfile getSoundProfileById(in String id); List<SoundProfile> getSoundProfilesByPackage(in String packageName); List<SoundProfile> getAvailableSoundProfiles(); - List<SoundProfile> getAllSoundProfiles(); + List<String> getSoundProfilePackageNames(); void registerPictureProfileCallback(in IPictureProfileCallback cb); void registerSoundProfileCallback(in ISoundProfileCallback cb); diff --git a/media/java/android/media/quality/IPictureProfileCallback.aidl b/media/java/android/media/quality/IPictureProfileCallback.aidl index 05441cde31e7..34aa2b061caf 100644 --- a/media/java/android/media/quality/IPictureProfileCallback.aidl +++ b/media/java/android/media/quality/IPictureProfileCallback.aidl @@ -17,6 +17,7 @@ package android.media.quality; +import android.media.quality.ParamCapability; import android.media.quality.PictureProfile; /** @@ -24,7 +25,9 @@ import android.media.quality.PictureProfile; * @hide */ oneway interface IPictureProfileCallback { - void onPictureProfileAdded(in long id, in PictureProfile p); - void onPictureProfileUpdated(in long id, in PictureProfile p); - void onPictureProfileRemoved(in long id, in PictureProfile p); + void onPictureProfileAdded(in String id, in PictureProfile p); + void onPictureProfileUpdated(in String id, in PictureProfile p); + void onPictureProfileRemoved(in String id, in PictureProfile p); + void onParamCapabilitiesChanged(in String id, in List<ParamCapability> caps); + void onError(in int err); } diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java index 38a2025535f4..26d83aca3e7b 100644 --- a/media/java/android/media/quality/MediaQualityManager.java +++ b/media/java/android/media/quality/MediaQualityManager.java @@ -19,6 +19,7 @@ package android.media.quality; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemService; import android.content.Context; import android.media.tv.flags.Flags; @@ -63,7 +64,7 @@ public final class MediaQualityManager { mService = service; IPictureProfileCallback ppCallback = new IPictureProfileCallback.Stub() { @Override - public void onPictureProfileAdded(long profileId, PictureProfile profile) { + public void onPictureProfileAdded(String profileId, PictureProfile profile) { synchronized (mLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { // TODO: filter callback record @@ -72,7 +73,7 @@ public final class MediaQualityManager { } } @Override - public void onPictureProfileUpdated(long profileId, PictureProfile profile) { + public void onPictureProfileUpdated(String profileId, PictureProfile profile) { synchronized (mLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { // TODO: filter callback record @@ -81,7 +82,7 @@ public final class MediaQualityManager { } } @Override - public void onPictureProfileRemoved(long profileId, PictureProfile profile) { + public void onPictureProfileRemoved(String profileId, PictureProfile profile) { synchronized (mLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { // TODO: filter callback record @@ -89,6 +90,24 @@ public final class MediaQualityManager { } } } + @Override + public void onParamCapabilitiesChanged(String profileId, List<ParamCapability> caps) { + synchronized (mLock) { + for (PictureProfileCallbackRecord record : mPpCallbackRecords) { + // TODO: filter callback record + record.postParamCapabilitiesChanged(profileId, caps); + } + } + } + @Override + public void onError(int err) { + synchronized (mLock) { + for (PictureProfileCallbackRecord record : mPpCallbackRecords) { + // TODO: filter callback record + record.postError(err); + } + } + } }; ISoundProfileCallback spCallback = new ISoundProfileCallback.Stub() { @Override @@ -175,14 +194,17 @@ public final class MediaQualityManager { /** - * Gets picture profile by given profile ID. - * @return the corresponding picture profile if available; {@code null} if the ID doesn't - * exist or the profile is not accessible to the caller. + * Gets picture profile by given profile type and name. + * + * @return the corresponding picture profile if available; {@code null} if the name doesn't + * exist. * @hide */ - public PictureProfile getPictureProfileById(long profileId) { + @Nullable + public PictureProfile getPictureProfile( + @PictureProfile.ProfileType int type, @NonNull String name) { try { - return mService.getPictureProfileById(profileId); + return mService.getPictureProfile(type, name); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -190,11 +212,13 @@ public final class MediaQualityManager { /** - * @SystemApi gets profiles that available to the given package - * @hide + * Gets profiles that available to the given package. + * + * @hide @SystemApi */ + @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) - public List<PictureProfile> getPictureProfilesByPackage(String packageName) { + public List<PictureProfile> getPictureProfilesByPackage(@NonNull String packageName) { try { return mService.getPictureProfilesByPackage(packageName); } catch (RemoteException e) { @@ -215,13 +239,16 @@ public final class MediaQualityManager { } /** - * @SystemApi all stored picture profiles of all packages - * @hide + * Gets all package names whose picture profiles are available. + * + * @see #getPictureProfilesByPackage(String) + * @hide @SystemApi */ + @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) - public List<PictureProfile> getAllPictureProfiles() { + public List<String> getPictureProfilePackageNames() { try { - return mService.getAllPictureProfiles(); + return mService.getPictureProfilePackageNames(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -231,10 +258,12 @@ public final class MediaQualityManager { /** * Creates a picture profile and store it in the system. * - * @return the stored profile with an assigned profile ID. + * @return the stored profile with an assigned profile ID. {@code null} if it's not created + * successfully. * @hide */ - public PictureProfile createPictureProfile(PictureProfile pp) { + @Nullable + public PictureProfile createPictureProfile(@NonNull PictureProfile pp) { try { return mService.createPictureProfile(pp); } catch (RemoteException e) { @@ -247,7 +276,7 @@ public final class MediaQualityManager { * Updates an existing picture profile and store it in the system. * @hide */ - public void updatePictureProfile(long profileId, PictureProfile pp) { + public void updatePictureProfile(@NonNull String profileId, @NonNull PictureProfile pp) { try { mService.updatePictureProfile(profileId, pp); } catch (RemoteException e) { @@ -260,7 +289,7 @@ public final class MediaQualityManager { * Removes a picture profile from the system. * @hide */ - public void removePictureProfile(long profileId) { + public void removePictureProfile(@NonNull String profileId) { try { mService.removePictureProfile(profileId); } catch (RemoteException e) { @@ -307,7 +336,7 @@ public final class MediaQualityManager { * exist or the profile is not accessible to the caller. * @hide */ - public SoundProfile getSoundProfileById(long profileId) { + public SoundProfile getSoundProfileById(String profileId) { try { return mService.getSoundProfileById(profileId); } catch (RemoteException e) { @@ -346,9 +375,9 @@ public final class MediaQualityManager { * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) - public List<SoundProfile> getAllSoundProfiles() { + public List<String> getSoundProfilePackageNames() { try { - return mService.getAllSoundProfiles(); + return mService.getSoundProfilePackageNames(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -374,7 +403,7 @@ public final class MediaQualityManager { * Updates an existing sound profile and store it in the system. * @hide */ - public void updateSoundProfile(long profileId, SoundProfile sp) { + public void updateSoundProfile(String profileId, SoundProfile sp) { try { mService.updateSoundProfile(profileId, sp); } catch (RemoteException e) { @@ -387,7 +416,7 @@ public final class MediaQualityManager { * Removes a sound profile from the system. * @hide */ - public void removeSoundProfile(long profileId) { + public void removeSoundProfile(String profileId) { try { mService.removeSoundProfile(profileId); } catch (RemoteException e) { @@ -399,7 +428,8 @@ public final class MediaQualityManager { * Gets capability information of the given parameters. * @hide */ - public List<ParamCapability> getParamCapabilities(List<String> names) { + @NonNull + public List<ParamCapability> getParamCapabilities(@NonNull List<String> names) { try { return mService.getParamCapabilities(names); } catch (RemoteException e) { @@ -408,7 +438,38 @@ public final class MediaQualityManager { } /** + * Gets the allowlist of packages that can create and removed picture profiles + * + * @see #createPictureProfile(PictureProfile) + * @see #removePictureProfile(String) + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) + @NonNull + public List<String> getPictureProfileAllowList() { + try { + return mService.getPictureProfileAllowList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sets the allowlist of packages that can create and removed picture profiles + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) + public void setPictureProfileAllowList(@NonNull List<String> packageNames) { + try { + mService.setPictureProfileAllowList(packageNames); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Returns {@code true} if media quality HAL is implemented; {@code false} otherwise. + * @hide */ public boolean isSupported() { try { @@ -581,7 +642,7 @@ public final class MediaQualityManager { return mCallback; } - public void postPictureProfileAdded(final long id, PictureProfile profile) { + public void postPictureProfileAdded(final String id, PictureProfile profile) { mExecutor.execute(new Runnable() { @Override @@ -591,7 +652,7 @@ public final class MediaQualityManager { }); } - public void postPictureProfileUpdated(final long id, PictureProfile profile) { + public void postPictureProfileUpdated(final String id, PictureProfile profile) { mExecutor.execute(new Runnable() { @Override public void run() { @@ -600,7 +661,7 @@ public final class MediaQualityManager { }); } - public void postPictureProfileRemoved(final long id, PictureProfile profile) { + public void postPictureProfileRemoved(final String id, PictureProfile profile) { mExecutor.execute(new Runnable() { @Override public void run() { @@ -608,6 +669,24 @@ public final class MediaQualityManager { } }); } + + public void postParamCapabilitiesChanged(final String id, List<ParamCapability> caps) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onParamCapabilitiesChanged(id, caps); + } + }); + } + + public void postError(int error) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onError(error); + } + }); + } } private static final class SoundProfileCallbackRecord { @@ -681,24 +760,57 @@ public final class MediaQualityManager { */ public abstract static class PictureProfileCallback { /** + * This is invoked when a picture profile has been added. + * + * @param profileId the ID of the profile. + * @param profile the newly added profile. * @hide */ - public void onPictureProfileAdded(long id, PictureProfile profile) { + public void onPictureProfileAdded( + @NonNull String profileId, @NonNull PictureProfile profile) { } + /** + * This is invoked when a picture profile has been updated. + * + * @param profileId the ID of the profile. + * @param profile the profile with updated info. * @hide */ - public void onPictureProfileUpdated(long id, PictureProfile profile) { + public void onPictureProfileUpdated( + @NonNull String profileId, @NonNull PictureProfile profile) { } + /** + * This is invoked when a picture profile has been removed. + * + * @param profileId the ID of the profile. + * @param profile the removed profile. * @hide */ - public void onPictureProfileRemoved(long id, PictureProfile profile) { + public void onPictureProfileRemoved( + @NonNull String profileId, @NonNull PictureProfile profile) { } + /** + * This is invoked when an issue has occurred. + * + * @param errorCode the error code * @hide */ - public void onError(int errorCode) { + public void onError(@PictureProfile.ErrorCode int errorCode) { + } + + /** + * This is invoked when parameter capabilities has been changed due to status changes of the + * content. + * + * @param profileId the ID of the profile used by the media content. + * @param updatedCaps the updated capabilities. + * @hide + */ + public void onParamCapabilitiesChanged( + @NonNull String profileId, @NonNull List<ParamCapability> updatedCaps) { } } diff --git a/media/java/android/media/quality/ParamCapability.java b/media/java/android/media/quality/ParamCapability.java index 70e85920c12f..0b698a9c1ad2 100644 --- a/media/java/android/media/quality/ParamCapability.java +++ b/media/java/android/media/quality/ParamCapability.java @@ -34,7 +34,7 @@ import java.lang.annotation.RetentionPolicy; * @hide */ @FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW) -public class ParamCapability implements Parcelable { +public final class ParamCapability implements Parcelable { /** @hide */ @IntDef(flag = true, prefix = { "TYPE_" }, value = { @@ -104,6 +104,7 @@ public class ParamCapability implements Parcelable { @NonNull private final Bundle mCaps; + /** @hide */ protected ParamCapability(Parcel in) { mName = in.readString(); mIsSupported = in.readBoolean(); @@ -112,7 +113,7 @@ public class ParamCapability implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mName); dest.writeBoolean(mIsSupported); dest.writeInt(mType); @@ -124,6 +125,7 @@ public class ParamCapability implements Parcelable { return 0; } + @NonNull public static final Creator<ParamCapability> CREATOR = new Creator<ParamCapability>() { @Override public ParamCapability createFromParcel(Parcel in) { diff --git a/media/java/android/media/quality/PictureProfile.java b/media/java/android/media/quality/PictureProfile.java index 8fb57124d33e..2be47dd87ef2 100644 --- a/media/java/android/media/quality/PictureProfile.java +++ b/media/java/android/media/quality/PictureProfile.java @@ -71,6 +71,53 @@ public final class PictureProfile implements Parcelable { */ public static final int TYPE_APPLICATION = 2; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, prefix = "ERROR_", value = { + ERROR_UNKNOWN, + ERROR_NO_PERMISSION, + ERROR_DUPLICATE, + ERROR_INVALID_ARGUMENT, + ERROR_NOT_ALLOWLISTED + }) + public @interface ErrorCode {} + + /** + * Error code for unknown errors. + * @hide + */ + public static final int ERROR_UNKNOWN = 0; + + /** + * Error code for missing necessary permission to handle the profiles. + * @hide + */ + public static final int ERROR_NO_PERMISSION = 1; + + /** + * Error code for creating a profile with existing profile type and name. + * + * @see #getProfileType() + * @see #getName() + * @hide + */ + public static final int ERROR_DUPLICATE = 2; + + /** + * Error code for invalid argument. + * @hide + */ + public static final int ERROR_INVALID_ARGUMENT = 3; + + /** + * Error code for the case when an operation requires an allowlist but the caller is not in the + * list. + * + * @see MediaQualityManager#getPictureProfileAllowList() + * @hide + */ + public static final int ERROR_NOT_ALLOWLISTED = 4; + private PictureProfile(@NonNull Parcel in) { mId = in.readString(); diff --git a/native/android/Android.bp b/native/android/Android.bp index 3eb99c3387f7..da29c49f9d7b 100644 --- a/native/android/Android.bp +++ b/native/android/Android.bp @@ -55,6 +55,7 @@ cc_library_shared { "surface_control_input_receiver.cpp", "choreographer.cpp", "configuration.cpp", + "dynamic_instrumentation_manager.cpp", "hardware_buffer_jni.cpp", "input.cpp", "input_transfer_token.cpp", @@ -100,6 +101,7 @@ cc_library_shared { "android.hardware.configstore@1.0", "android.hardware.configstore-utils", "android.os.flags-aconfig-cc", + "dynamic_instrumentation_manager_aidl-cpp", "libnativedisplay", "libfmq", ], diff --git a/native/android/dynamic_instrumentation_manager.cpp b/native/android/dynamic_instrumentation_manager.cpp new file mode 100644 index 000000000000..d9bacb116f96 --- /dev/null +++ b/native/android/dynamic_instrumentation_manager.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "ADynamicInstrumentationManager" +#include <android/dynamic_instrumentation_manager.h> +#include <android/os/instrumentation/ExecutableMethodFileOffsets.h> +#include <android/os/instrumentation/IDynamicInstrumentationManager.h> +#include <android/os/instrumentation/MethodDescriptor.h> +#include <android/os/instrumentation/TargetProcess.h> +#include <binder/Binder.h> +#include <binder/IServiceManager.h> +#include <utils/Log.h> + +#include <mutex> +#include <optional> +#include <string> +#include <vector> + +namespace android::dynamicinstrumentationmanager { + +// Global instance of IDynamicInstrumentationManager, service is obtained only on first use. +static std::mutex mLock; +static sp<os::instrumentation::IDynamicInstrumentationManager> mService; + +sp<os::instrumentation::IDynamicInstrumentationManager> getService() { + std::lock_guard<std::mutex> scoped_lock(mLock); + if (mService == nullptr || !IInterface::asBinder(mService)->isBinderAlive()) { + sp<IBinder> binder = + defaultServiceManager()->waitForService(String16("dynamic_instrumentation")); + mService = interface_cast<os::instrumentation::IDynamicInstrumentationManager>(binder); + } + return mService; +} + +} // namespace android::dynamicinstrumentationmanager + +using namespace android; +using namespace dynamicinstrumentationmanager; + +struct ADynamicInstrumentationManager_TargetProcess { + uid_t uid; + uid_t pid; + std::string processName; + + ADynamicInstrumentationManager_TargetProcess(uid_t uid, pid_t pid, const char* processName) + : uid(uid), pid(pid), processName(processName) {} +}; + +ADynamicInstrumentationManager_TargetProcess* ADynamicInstrumentationManager_TargetProcess_create( + uid_t uid, pid_t pid, const char* processName) { + return new ADynamicInstrumentationManager_TargetProcess(uid, pid, processName); +} + +void ADynamicInstrumentationManager_TargetProcess_destroy( + ADynamicInstrumentationManager_TargetProcess* instance) { + delete instance; +} + +struct ADynamicInstrumentationManager_MethodDescriptor { + std::string fqcn; + std::string methodName; + std::vector<std::string> fqParameters; + + ADynamicInstrumentationManager_MethodDescriptor(const char* fqcn, const char* methodName, + const char* fullyQualifiedParameters[], + size_t numParameters) + : fqcn(fqcn), methodName(methodName) { + std::vector<std::string> fqParameters; + fqParameters.reserve(numParameters); + std::copy_n(fullyQualifiedParameters, numParameters, std::back_inserter(fqParameters)); + this->fqParameters = std::move(fqParameters); + } +}; + +ADynamicInstrumentationManager_MethodDescriptor* +ADynamicInstrumentationManager_MethodDescriptor_create(const char* fullyQualifiedClassName, + const char* methodName, + const char* fullyQualifiedParameters[], + size_t numParameters) { + return new ADynamicInstrumentationManager_MethodDescriptor(fullyQualifiedClassName, methodName, + fullyQualifiedParameters, + numParameters); +} + +void ADynamicInstrumentationManager_MethodDescriptor_destroy( + ADynamicInstrumentationManager_MethodDescriptor* instance) { + delete instance; +} + +struct ADynamicInstrumentationManager_ExecutableMethodFileOffsets { + std::string containerPath; + uint64_t containerOffset; + uint64_t methodOffset; +}; + +ADynamicInstrumentationManager_ExecutableMethodFileOffsets* +ADynamicInstrumentationManager_ExecutableMethodFileOffsets_create() { + return new ADynamicInstrumentationManager_ExecutableMethodFileOffsets(); +} + +const char* ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerPath( + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) { + return instance->containerPath.c_str(); +} + +uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerOffset( + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) { + return instance->containerOffset; +} + +uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOffset( + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) { + return instance->methodOffset; +} + +void ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy( + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) { + delete instance; +} + +int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets( + const ADynamicInstrumentationManager_TargetProcess* targetProcess, + const ADynamicInstrumentationManager_MethodDescriptor* methodDescriptor, + ADynamicInstrumentationManager_ExecutableMethodFileOffsets** out) { + android::os::instrumentation::TargetProcess targetProcessParcel; + targetProcessParcel.uid = targetProcess->uid; + targetProcessParcel.pid = targetProcess->pid; + targetProcessParcel.processName = targetProcess->processName; + + android::os::instrumentation::MethodDescriptor methodDescriptorParcel; + methodDescriptorParcel.fullyQualifiedClassName = methodDescriptor->fqcn; + methodDescriptorParcel.methodName = methodDescriptor->methodName; + methodDescriptorParcel.fullyQualifiedParameters = methodDescriptor->fqParameters; + + sp<os::instrumentation::IDynamicInstrumentationManager> service = getService(); + if (service == nullptr) { + return INVALID_OPERATION; + } + + std::optional<android::os::instrumentation::ExecutableMethodFileOffsets> offsets; + binder_status_t result = + service->getExecutableMethodFileOffsets(targetProcessParcel, methodDescriptorParcel, + &offsets) + .exceptionCode(); + if (result != OK) { + return result; + } + + if (offsets != std::nullopt) { + auto* value = new ADynamicInstrumentationManager_ExecutableMethodFileOffsets(); + value->containerPath = offsets->containerPath; + value->containerOffset = offsets->containerOffset; + value->methodOffset = offsets->methodOffset; + *out = value; + } else { + *out = nullptr; + } + + return result; +}
\ No newline at end of file diff --git a/native/android/include_platform/android/dynamic_instrumentation_manager.h b/native/android/include_platform/android/dynamic_instrumentation_manager.h new file mode 100644 index 000000000000..6c46288954bf --- /dev/null +++ b/native/android/include_platform/android/dynamic_instrumentation_manager.h @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ADYNAMICINSTRUMENTATIONMANAGER_H__ +#define __ADYNAMICINSTRUMENTATIONMANAGER_H__ + +#include <sys/cdefs.h> +#include <sys/types.h> + +__BEGIN_DECLS + +struct ADynamicInstrumentationManager_MethodDescriptor; +typedef struct ADynamicInstrumentationManager_MethodDescriptor + ADynamicInstrumentationManager_MethodDescriptor; + +struct ADynamicInstrumentationManager_TargetProcess; +typedef struct ADynamicInstrumentationManager_TargetProcess + ADynamicInstrumentationManager_TargetProcess; + +struct ADynamicInstrumentationManager_ExecutableMethodFileOffsets; +typedef struct ADynamicInstrumentationManager_ExecutableMethodFileOffsets + ADynamicInstrumentationManager_ExecutableMethodFileOffsets; + +/** + * Initializes an ADynamicInstrumentationManager_TargetProcess. Caller must clean up when they are + * done with ADynamicInstrumentationManager_TargetProcess_destroy. + * + * @param uid of targeted process. + * @param pid of targeted process. + * @param processName to disambiguate from corner cases that may arise from pid reuse. + */ +ADynamicInstrumentationManager_TargetProcess* _Nonnull + ADynamicInstrumentationManager_TargetProcess_create( + uid_t uid, pid_t pid, const char* _Nonnull processName) __INTRODUCED_IN(36); +/** + * Clean up an ADynamicInstrumentationManager_TargetProcess. + * + * @param instance returned from ADynamicInstrumentationManager_TargetProcess_create. + */ +void ADynamicInstrumentationManager_TargetProcess_destroy( + ADynamicInstrumentationManager_TargetProcess* _Nonnull instance) __INTRODUCED_IN(36); + +/** + * Initializes an ADynamicInstrumentationManager_MethodDescriptor. Caller must clean up when they + * are done with ADynamicInstrumentationManager_MethodDescriptor_Destroy. + * + * @param fullyQualifiedClassName fqcn of class containing the method. + * @param methodName + * @param fullyQualifiedParameters fqcn of parameters of the method's signature, or e.g. "int" for + * primitives. + * @param numParameters length of `fullyQualifiedParameters` array. + */ +ADynamicInstrumentationManager_MethodDescriptor* _Nonnull + ADynamicInstrumentationManager_MethodDescriptor_create( + const char* _Nonnull fullyQualifiedClassName, const char* _Nonnull methodName, + const char* _Nonnull fullyQualifiedParameters[_Nonnull], size_t numParameters) + __INTRODUCED_IN(36); +/** + * Clean up an ADynamicInstrumentationManager_MethodDescriptor. + * + * @param instance returned from ADynamicInstrumentationManager_MethodDescriptor_create. + */ +void ADynamicInstrumentationManager_MethodDescriptor_destroy( + ADynamicInstrumentationManager_MethodDescriptor* _Nonnull instance) __INTRODUCED_IN(36); + +/** + * Get the containerPath calculated by + * ADynamicInstrumentationManager_getExecutableMethodFileOffsets. + * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets. + * @return The OS path of the containing file. + */ +const char* _Nullable ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerPath( + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance) + __INTRODUCED_IN(36); +/** + * Get the containerOffset calculated by + * ADynamicInstrumentationManager_getExecutableMethodFileOffsets. + * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets. + * @return The offset of the containing file within the process' memory. + */ +uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerOffset( + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance) + __INTRODUCED_IN(36); +/** + * Get the methodOffset calculated by ADynamicInstrumentationManager_getExecutableMethodFileOffsets. + * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets. + * @return The offset of the method within the containing file. + */ +uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOffset( + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance) + __INTRODUCED_IN(36); +/** + * Clean up an ADynamicInstrumentationManager_ExecutableMethodFileOffsets. + * + * @param instance returned from ADynamicInstrumentationManager_getExecutableMethodFileOffsets. + */ +void ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy( + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance) + __INTRODUCED_IN(36); +/** + * Provides ART metadata about the described java method within the target process. + * + * @param targetProcess describes for which process the data is requested. + * @param methodDescriptor describes the targeted method. + * @param out will be populated with the data if successful. A nullptr combined + * with an OK status means that the program method is defined, but the offset + * info was unavailable because it is not AOT compiled. + * @return status indicating success or failure. The values correspond to the `binder_exception_t` + * enum values from <android/binder_status.h>. + */ +int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets( + const ADynamicInstrumentationManager_TargetProcess* _Nonnull targetProcess, + const ADynamicInstrumentationManager_MethodDescriptor* _Nonnull methodDescriptor, + ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull* _Nullable out) + __INTRODUCED_IN(36); + +__END_DECLS + +#endif // __ADYNAMICINSTRUMENTATIONMANAGER_H__ diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index b025cb880ee7..a0460572abfc 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -4,6 +4,15 @@ LIBANDROID { AActivityManager_removeUidImportanceListener; # systemapi introduced=31 AActivityManager_isUidActive; # systemapi introduced=31 AActivityManager_getUidImportance; # systemapi introduced=31 + ADynamicInstrumentationManager_TargetProcess_create; # systemapi + ADynamicInstrumentationManager_TargetProcess_destroy; # systemapi + ADynamicInstrumentationManager_MethodDescriptor_create; # systemapi + ADynamicInstrumentationManager_MethodDescriptor_destroy; # systemapi + ADynamicInstrumentationManager_getExecutableMethodFileOffsets; # systemapi + ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerPath; # systemapi + ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerOffset; # systemapi + ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOffset; # systemapi + ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy; # systemapi AAssetDir_close; AAssetDir_getNextFileName; AAssetDir_rewind; diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index 675c8f80add2..a23845fa17e9 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -100,6 +100,7 @@ package android.nfc { method public void onHceEventReceived(int); method public void onLaunchHceAppChooserActivity(@NonNull String, @NonNull java.util.List<android.nfc.cardemulation.ApduServiceInfo>, @NonNull android.content.ComponentName, @NonNull String); method public void onLaunchHceTapAgainDialog(@NonNull android.nfc.cardemulation.ApduServiceInfo, @NonNull String); + method public void onLogEventNotified(@NonNull android.nfc.OemLogItems); method public void onNdefMessage(@NonNull android.nfc.Tag, @NonNull android.nfc.NdefMessage, @NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onNdefRead(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onReaderOptionChanged(boolean); @@ -115,6 +116,27 @@ package android.nfc { method public int getNfceeId(); } + @FlaggedApi("android.nfc.nfc_oem_extension") public final class OemLogItems implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public int getCallingPid(); + method @Nullable public byte[] getCommandApdu(); + method public int getEvent(); + method @Nullable public byte[] getResponseApdu(); + method @Nullable public java.time.Instant getRfFieldEventTimeMillis(); + method @Nullable public android.nfc.Tag getTag(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.nfc.OemLogItems> CREATOR; + field public static final int EVENT_DISABLE = 2; // 0x2 + field public static final int EVENT_ENABLE = 1; // 0x1 + field public static final int EVENT_UNSET = 0; // 0x0 + field public static final int LOG_ACTION_HCE_DATA = 516; // 0x204 + field public static final int LOG_ACTION_NFC_TOGGLE = 513; // 0x201 + field public static final int LOG_ACTION_RF_FIELD_STATE_CHANGED = 1; // 0x1 + field public static final int LOG_ACTION_SCREEN_STATE_CHANGED = 518; // 0x206 + field public static final int LOG_ACTION_TAG_DETECTED = 3; // 0x3 + } + @FlaggedApi("android.nfc.nfc_oem_extension") public class RoutingStatus { method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultIsoDepRoute(); method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultOffHostRoute(); diff --git a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl index 7f1fd15fe68a..b102e873d737 100644 --- a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl +++ b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl @@ -18,6 +18,7 @@ package android.nfc; import android.content.ComponentName; import android.nfc.cardemulation.ApduServiceInfo; import android.nfc.NdefMessage; +import android.nfc.OemLogItems; import android.nfc.Tag; import android.os.ResultReceiver; @@ -51,4 +52,5 @@ interface INfcOemExtensionCallback { void onNdefMessage(in Tag tag, in NdefMessage message, in ResultReceiver hasOemExecutableContent); void onLaunchHceAppChooserActivity(in String selectedAid, in List<ApduServiceInfo> services, in ComponentName failedComponent, in String category); void onLaunchHceTapAgainActivity(in ApduServiceInfo service, in String category); + void onLogEventNotified(in OemLogItems item); } diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java index 1bfe71461ac3..abd99bc02f55 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -392,6 +392,12 @@ public final class NfcOemExtension { * @param category the category of the service */ void onLaunchHceTapAgainDialog(@NonNull ApduServiceInfo service, @NonNull String category); + + /** + * Callback when OEM specified log event are notified. + * @param item the log items that contains log information of NFC event. + */ + void onLogEventNotified(@NonNull OemLogItems item); } @@ -900,6 +906,12 @@ public final class NfcOemExtension { handleVoid2ArgCallback(service, category, cb::onLaunchHceTapAgainDialog, ex)); } + @Override + public void onLogEventNotified(OemLogItems item) throws RemoteException { + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(item, cb::onLogEventNotified, ex)); + } + private <T> void handleVoidCallback( T input, Consumer<T> callbackMethod, Executor executor) { synchronized (mLock) { diff --git a/nfc/java/android/nfc/OemLogItems.aidl b/nfc/java/android/nfc/OemLogItems.aidl new file mode 100644 index 000000000000..3bcb445fc7d2 --- /dev/null +++ b/nfc/java/android/nfc/OemLogItems.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable OemLogItems;
\ No newline at end of file diff --git a/nfc/java/android/nfc/OemLogItems.java b/nfc/java/android/nfc/OemLogItems.java new file mode 100644 index 000000000000..6671941c1cc9 --- /dev/null +++ b/nfc/java/android/nfc/OemLogItems.java @@ -0,0 +1,325 @@ +/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nfc;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+
+/**
+ * A log class for OEMs to get log information of NFC events.
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+@SystemApi
+public final class OemLogItems implements Parcelable {
+ /**
+ * Used when RF field state is changed.
+ */
+ public static final int LOG_ACTION_RF_FIELD_STATE_CHANGED = 0X01;
+ /**
+ * Used when NFC is toggled. Event should be set to {@link LogEvent#EVENT_ENABLE} or
+ * {@link LogEvent#EVENT_DISABLE} if this action is used.
+ */
+ public static final int LOG_ACTION_NFC_TOGGLE = 0x0201;
+ /**
+ * Used when sending host routing status.
+ */
+ public static final int LOG_ACTION_HCE_DATA = 0x0204;
+ /**
+ * Used when screen state is changed.
+ */
+ public static final int LOG_ACTION_SCREEN_STATE_CHANGED = 0x0206;
+ /**
+ * Used when tag is detected.
+ */
+ public static final int LOG_ACTION_TAG_DETECTED = 0x03;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "LOG_ACTION_" }, value = {
+ LOG_ACTION_RF_FIELD_STATE_CHANGED,
+ LOG_ACTION_NFC_TOGGLE,
+ LOG_ACTION_HCE_DATA,
+ LOG_ACTION_SCREEN_STATE_CHANGED,
+ LOG_ACTION_TAG_DETECTED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LogAction {}
+
+ /**
+ * Represents the event is not set.
+ */
+ public static final int EVENT_UNSET = 0;
+ /**
+ * Represents nfc enable is called.
+ */
+ public static final int EVENT_ENABLE = 1;
+ /**
+ * Represents nfc disable is called.
+ */
+ public static final int EVENT_DISABLE = 2;
+ /** @hide */
+ @IntDef(prefix = { "EVENT_" }, value = {
+ EVENT_UNSET,
+ EVENT_ENABLE,
+ EVENT_DISABLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LogEvent {}
+ private int mAction;
+ private int mEvent;
+ private int mCallingPid;
+ private byte[] mCommandApdus;
+ private byte[] mResponseApdus;
+ private Instant mRfFieldOnTime;
+ private Tag mTag;
+
+ /** @hide */
+ public OemLogItems(@LogAction int action, @LogEvent int event, int callingPid,
+ byte[] commandApdus, byte[] responseApdus, Instant rfFieldOnTime,
+ Tag tag) {
+ mAction = action;
+ mEvent = event;
+ mTag = tag;
+ mCallingPid = callingPid;
+ mCommandApdus = commandApdus;
+ mResponseApdus = responseApdus;
+ mRfFieldOnTime = rfFieldOnTime;
+ }
+
+ /**
+ * Describe the kinds of special objects contained in this Parcelable
+ * instance's marshaled representation. For example, if the object will
+ * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
+ * the return value of this method must include the
+ * {@link #CONTENTS_FILE_DESCRIPTOR} bit.
+ *
+ * @return a bitmask indicating the set of special object types marshaled
+ * by this Parcelable object instance.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mAction);
+ dest.writeInt(mEvent);
+ dest.writeInt(mCallingPid);
+ dest.writeInt(mCommandApdus.length);
+ dest.writeByteArray(mCommandApdus);
+ dest.writeInt(mResponseApdus.length);
+ dest.writeByteArray(mResponseApdus);
+ dest.writeLong(mRfFieldOnTime.getEpochSecond());
+ dest.writeInt(mRfFieldOnTime.getNano());
+ dest.writeParcelable(mTag, 0);
+ }
+
+ /** @hide */
+ public static class Builder {
+ private final OemLogItems mItem;
+
+ public Builder(@LogAction int type) {
+ mItem = new OemLogItems(type, EVENT_UNSET, 0, new byte[0], new byte[0], null, null);
+ }
+
+ /** Setter of the log action. */
+ public OemLogItems.Builder setAction(@LogAction int action) {
+ mItem.mAction = action;
+ return this;
+ }
+
+ /** Setter of the log calling event. */
+ public OemLogItems.Builder setCallingEvent(@LogEvent int event) {
+ mItem.mEvent = event;
+ return this;
+ }
+
+ /** Setter of the log calling Pid. */
+ public OemLogItems.Builder setCallingPid(int pid) {
+ mItem.mCallingPid = pid;
+ return this;
+ }
+
+ /** Setter of APDU command. */
+ public OemLogItems.Builder setApduCommand(byte[] apdus) {
+ mItem.mCommandApdus = apdus;
+ return this;
+ }
+
+ /** Setter of RF field on time. */
+ public OemLogItems.Builder setRfFieldOnTime(Instant time) {
+ mItem.mRfFieldOnTime = time;
+ return this;
+ }
+
+ /** Setter of APDU response. */
+ public OemLogItems.Builder setApduResponse(byte[] apdus) {
+ mItem.mResponseApdus = apdus;
+ return this;
+ }
+
+ /** Setter of dispatched tag. */
+ public OemLogItems.Builder setTag(Tag tag) {
+ mItem.mTag = tag;
+ return this;
+ }
+
+ /** Builds an {@link OemLogItems} instance. */
+ public OemLogItems build() {
+ return mItem;
+ }
+ }
+
+ /**
+ * Gets the action of this log.
+ * @return one of {@link LogAction}
+ */
+ @LogAction
+ public int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Gets the event of this log. This will be set to {@link LogEvent#EVENT_ENABLE} or
+ * {@link LogEvent#EVENT_DISABLE} only when action is set to
+ * {@link LogAction#LOG_ACTION_NFC_TOGGLE}
+ * @return one of {@link LogEvent}
+ */
+ @LogEvent
+ public int getEvent() {
+ return mEvent;
+ }
+
+ /**
+ * Gets the calling Pid of this log. This field will be set only when action is set to
+ * {@link LogAction#LOG_ACTION_NFC_TOGGLE}
+ * @return calling Pid
+ */
+ public int getCallingPid() {
+ return mCallingPid;
+ }
+
+ /**
+ * Gets the command APDUs of this log. This field will be set only when action is set to
+ * {@link LogAction#LOG_ACTION_HCE_DATA}
+ * @return a byte array of command APDUs with the same format as
+ * {@link android.nfc.cardemulation.HostApduService#sendResponseApdu(byte[])}
+ */
+ @Nullable
+ public byte[] getCommandApdu() {
+ return mCommandApdus;
+ }
+
+ /**
+ * Gets the response APDUs of this log. This field will be set only when action is set to
+ * {@link LogAction#LOG_ACTION_HCE_DATA}
+ * @return a byte array of response APDUs with the same format as
+ * {@link android.nfc.cardemulation.HostApduService#sendResponseApdu(byte[])}
+ */
+ @Nullable
+ public byte[] getResponseApdu() {
+ return mResponseApdus;
+ }
+
+ /**
+ * Gets the RF field event time in this log in millisecond. This field will be set only when
+ * action is set to {@link LogAction#LOG_ACTION_RF_FIELD_STATE_CHANGED}
+ * @return an {@link Instant} of RF field event time.
+ */
+ @Nullable
+ public Instant getRfFieldEventTimeMillis() {
+ return mRfFieldOnTime;
+ }
+
+ /**
+ * Gets the tag of this log. This field will be set only when action is set to
+ * {@link LogAction#LOG_ACTION_TAG_DETECTED}
+ * @return a detected {@link Tag} in {@link #LOG_ACTION_TAG_DETECTED} case. Return
+ * null otherwise.
+ */
+ @Nullable
+ public Tag getTag() {
+ return mTag;
+ }
+
+ private String byteToHex(byte[] bytes) {
+ char[] HexArray = "0123456789ABCDEF".toCharArray();
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = HexArray[v >>> 4];
+ hexChars[j * 2 + 1] = HexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ @Override
+ public String toString() {
+ return "[mCommandApdus: "
+ + ((mCommandApdus != null) ? byteToHex(mCommandApdus) : "null")
+ + "[mResponseApdus: "
+ + ((mResponseApdus != null) ? byteToHex(mResponseApdus) : "null")
+ + ", mCallingApi= " + mEvent
+ + ", mAction= " + mAction
+ + ", mCallingPId = " + mCallingPid
+ + ", mRfFieldOnTime= " + mRfFieldOnTime;
+ }
+ private OemLogItems(Parcel in) {
+ this.mAction = in.readInt();
+ this.mEvent = in.readInt();
+ this.mCallingPid = in.readInt();
+ this.mCommandApdus = new byte[in.readInt()];
+ in.readByteArray(this.mCommandApdus);
+ this.mResponseApdus = new byte[in.readInt()];
+ in.readByteArray(this.mResponseApdus);
+ this.mRfFieldOnTime = Instant.ofEpochSecond(in.readLong(), in.readInt());
+ this.mTag = in.readParcelable(Tag.class.getClassLoader(), Tag.class);
+ }
+
+ public static final @NonNull Parcelable.Creator<OemLogItems> CREATOR =
+ new Parcelable.Creator<OemLogItems>() {
+ @Override
+ public OemLogItems createFromParcel(Parcel in) {
+ return new OemLogItems(in);
+ }
+
+ @Override
+ public OemLogItems[] newArray(int size) {
+ return new OemLogItems[size];
+ }
+ };
+
+}
diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java index 3b52df7e5fbb..c3f6eb71c2e7 100644 --- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java +++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java @@ -30,21 +30,28 @@ public class AppPreference extends Preference { public AppPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - setLayoutResource(R.layout.preference_app); + init(context); } public AppPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - setLayoutResource(R.layout.preference_app); + init(context); } public AppPreference(Context context) { super(context); - setLayoutResource(R.layout.preference_app); + init(context); } public AppPreference(Context context, AttributeSet attrs) { super(context, attrs); - setLayoutResource(R.layout.preference_app); + init(context); + } + + private void init(Context context) { + int resId = SettingsThemeHelper.isExpressiveTheme(context) + ? com.android.settingslib.widget.theme.R.layout.settingslib_expressive_preference + : R.layout.preference_app; + setLayoutResource(resId); } } diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java index ecd500e1a160..3dcdfbaeb8b3 100644 --- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java +++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java @@ -32,22 +32,29 @@ public class AppSwitchPreference extends SwitchPreferenceCompat { public AppSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - setLayoutResource(R.layout.preference_app); + init(context); } public AppSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - setLayoutResource(R.layout.preference_app); + init(context); } public AppSwitchPreference(Context context, AttributeSet attrs) { super(context, attrs); - setLayoutResource(R.layout.preference_app); + init(context); } public AppSwitchPreference(Context context) { super(context); - setLayoutResource(R.layout.preference_app); + init(context); + } + + private void init(Context context) { + int resId = SettingsThemeHelper.isExpressiveTheme(context) + ? com.android.settingslib.widget.theme.R.layout.settingslib_expressive_preference + : R.layout.preference_app; + setLayoutResource(resId); } @Override diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt index 843d2aadf333..cd03dd7ca1b3 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt @@ -202,6 +202,12 @@ open class KeyedDataObservable<K> : KeyedObservable<K> { entry.value.execute { observer.onKeyChanged(key, reason) } } } + + fun hasAnyObserver(): Boolean { + synchronized(observers) { if (observers.isNotEmpty()) return true } + synchronized(keyedObservers) { if (keyedObservers.isNotEmpty()) return true } + return false + } } /** [KeyedObservable] with no-op implementations for all interfaces. */ diff --git a/packages/SettingsLib/IntroPreference/Android.bp b/packages/SettingsLib/IntroPreference/Android.bp index 155db186c702..8f9fb7a2e408 100644 --- a/packages/SettingsLib/IntroPreference/Android.bp +++ b/packages/SettingsLib/IntroPreference/Android.bp @@ -29,5 +29,6 @@ android_library { min_sdk_version: "21", apex_available: [ "//apex_available:platform", + "com.android.healthfitness", ], } diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt index 94d373bed0a5..bde4217b3962 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt @@ -136,6 +136,18 @@ class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) : for (it in children) action(it) } + /** Traversals preference hierarchy recursively and applies given action. */ + fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) { + action(this) + for (child in children) { + if (child is PreferenceHierarchy) { + child.forEachRecursively(action) + } else { + action(child) + } + } + } + /** Traversals preference hierarchy and applies given action. */ suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) { for (it in children) action(it) @@ -157,18 +169,7 @@ class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) : /** Returns all the [PreferenceHierarchyNode]s appear in the hierarchy. */ fun getAllPreferences(): List<PreferenceHierarchyNode> = - mutableListOf<PreferenceHierarchyNode>().also { getAllPreferences(it) } - - private fun getAllPreferences(result: MutableList<PreferenceHierarchyNode>) { - result.add(this) - for (child in children) { - if (child is PreferenceHierarchy) { - child.getAllPreferences(result) - } else { - result.add(child) - } - } - } + mutableListOf<PreferenceHierarchyNode>().apply { forEachRecursively { add(it) } } } /** diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt index 41a626fe8efa..991d5b7791e9 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt @@ -32,7 +32,7 @@ import com.android.settingslib.widget.SettingsBasePreferenceFragment open class PreferenceFragment : SettingsBasePreferenceFragment(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider { - private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null + protected var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceScreen = createPreferenceScreen() @@ -129,7 +129,9 @@ open class PreferenceFragment : } protected fun getPreferenceKeysInHierarchy(): Set<String> = - preferenceScreenBindingHelper?.getPreferences()?.map { it.metadata.key }?.toSet() ?: setOf() + preferenceScreenBindingHelper?.let { + mutableSetOf<String>().apply { it.forEachRecursively { add(it.metadata.key) } } + } ?: setOf() companion object { private const val TAG = "PreferenceFragment" diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 022fb1dbe99c..fbe892710d40 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -143,7 +143,8 @@ class PreferenceScreenBindingHelper( } } - fun getPreferences() = preferenceHierarchy.getAllPreferences() + fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) = + preferenceHierarchy.forEachRecursively(action) fun onCreate() { for (preference in lifecycleAwarePreferences) { @@ -191,11 +192,11 @@ class PreferenceScreenBindingHelper( companion object { /** Preference value is changed. */ - private const val CHANGE_REASON_VALUE = 0 + const val CHANGE_REASON_VALUE = 0 /** Preference state (title/summary, enable state, etc.) is changed. */ - private const val CHANGE_REASON_STATE = 1 + const val CHANGE_REASON_STATE = 1 /** Dependent preference state is changed. */ - private const val CHANGE_REASON_DEPENDENT = 2 + const val CHANGE_REASON_DEPENDENT = 2 /** Updates preference screen that has incomplete hierarchy. */ @JvmStatic diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp index 155ee831ae52..78e27fe5ad04 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp +++ b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp @@ -29,6 +29,7 @@ android_library { "//apex_available:platform", "com.android.permission", "com.android.mediaprovider", + "com.android.healthfitness", ], } @@ -51,5 +52,6 @@ java_aconfig_library { "//apex_available:platform", "com.android.permission", "com.android.mediaprovider", + "com.android.healthfitness", ], } diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml index ccdf37d452b0..0cd0b3cb14f1 100644 --- a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml +++ b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml @@ -22,7 +22,7 @@ android:minWidth="@dimen/settingslib_expressive_space_medium3" android:minHeight="@dimen/settingslib_expressive_space_medium3" android:gravity="center" - android:layout_marginEnd="-8dp" + android:layout_marginEnd="-4dp" android:filterTouchesWhenObscured="false"> <androidx.preference.internal.PreferenceImageView diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml index 3c69027c2080..cec8e45e2bfb 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml @@ -36,33 +36,34 @@ <style name="SettingsLibPreference.SwitchPreference" parent="SettingsSwitchPreference.SettingsLib"/> <style name="SettingsLibPreference.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> + <item name="layout">@layout/settingslib_expressive_preference</item> </style> <style name="SettingsLibPreference.Category.Expressive"> </style> <style name="SettingsLibPreference.CheckBoxPreference.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> + <item name="layout">@layout/settingslib_expressive_preference</item> </style> <style name="SettingsLibPreference.SwitchPreferenceCompat.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> + <item name="layout">@layout/settingslib_expressive_preference</item> <item name="android:widgetLayout">@layout/settingslib_expressive_preference_switch</item> </style> <style name="SettingsLibPreference.SeekBarPreference.Expressive"/> <style name="SettingsLibPreference.PreferenceScreen.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> + <item name="layout">@layout/settingslib_expressive_preference</item> </style> <style name="SettingsLibPreference.DialogPreference.Expressive"> + <item name="layout">@layout/settingslib_expressive_preference</item> </style> <style name="SettingsLibPreference.DialogPreference.EditTextPreference.Expressive"> - <item name="android:layout">@layout/settingslib_expressive_preference</item> - <item name="android:dialogLayout">@layout/settingslib_preference_dialog_edittext</item> + <item name="layout">@layout/settingslib_expressive_preference</item> + <item name="dialogLayout">@layout/settingslib_preference_dialog_edittext</item> </style> <style name="SettingsLibPreference.DropDown.Expressive"> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index 0dc772ab6ecd..ebd5a1deffd2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -224,7 +224,7 @@ public class BluetoothEventManager { // audio sharing is enabled. if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT && state == BluetoothAdapter.STATE_DISCONNECTED - && BluetoothUtils.isAudioSharingEnabled()) { + && BluetoothUtils.isAudioSharingUIAvailable(mContext)) { LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); if (profileManager != null && profileManager.getLeAudioBroadcastProfile() != null diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 612c193da9c3..a87b8153b858 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -64,6 +64,8 @@ public class BluetoothUtils { public static final int META_INT_ERROR = -1; public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled"; + public static final String DEVELOPER_OPTION_PREVIEW_KEY = + "bluetooth_le_audio_sharing_ui_preview_enabled"; private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH"; private static final Set<Integer> SA_PROFILES = @@ -643,6 +645,12 @@ public class BluetoothUtils { && connectedGroupIds.contains(groupId); } + /** Returns if the le audio sharing UI is available. */ + public static boolean isAudioSharingUIAvailable(@Nullable Context context) { + return isAudioSharingEnabled() || (context != null && isAudioSharingPreviewEnabled( + context.getContentResolver())); + } + /** Returns if the le audio sharing is enabled. */ public static boolean isAudioSharingEnabled() { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); @@ -653,7 +661,23 @@ public class BluetoothUtils { && adapter.isLeAudioBroadcastAssistantSupported() == BluetoothStatusCodes.FEATURE_SUPPORTED; } catch (IllegalStateException e) { - Log.d(TAG, "LE state is on, but there is no bluetooth service.", e); + Log.d(TAG, "Fail to check isAudioSharingEnabled, e = ", e); + return false; + } + } + + /** Returns if the le audio sharing preview is enabled in developer option. */ + public static boolean isAudioSharingPreviewEnabled(@Nullable ContentResolver contentResolver) { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + try { + return Flags.audioSharingDeveloperOption() + && getAudioSharingPreviewValue(contentResolver) + && adapter.isLeAudioBroadcastSourceSupported() + == BluetoothStatusCodes.FEATURE_SUPPORTED + && adapter.isLeAudioBroadcastAssistantSupported() + == BluetoothStatusCodes.FEATURE_SUPPORTED; + } catch (IllegalStateException e) { + Log.d(TAG, "Fail to check isAudioSharingPreviewEnabled, e = ", e); return false; } } @@ -996,6 +1020,17 @@ public class BluetoothUtils { BluetoothCsipSetCoordinator.GROUP_ID_INVALID); } + /** Get develop option value for audio sharing preview. */ + @WorkerThread + private static boolean getAudioSharingPreviewValue(@Nullable ContentResolver contentResolver) { + if (contentResolver == null) return false; + return Settings.Global.getInt( + contentResolver, + DEVELOPER_OPTION_PREVIEW_KEY, + 0 // value off + ) == 1; + } + /** Get secondary {@link CachedBluetoothDevice} in broadcast. */ @Nullable @WorkerThread diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 8641f7036c50..d0827b30efc9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -1245,7 +1245,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> */ public String getConnectionSummary(boolean shortSummary) { CharSequence summary = null; - if (BluetoothUtils.isAudioSharingEnabled()) { + if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) { if (mBluetoothManager == null) { mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index b9f16edf6e77..4b7cb36f2753 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -383,11 +383,7 @@ public class CsipDeviceManager { preferredMainDevice.refresh(); hasChanged = true; } - if (isWorkProfile()) { - log("addMemberDevicesIntoMainDevice: skip sync source for work profile"); - } else { - syncAudioSharingSourceIfNeeded(preferredMainDevice); - } + syncAudioSharingSourceIfNeeded(preferredMainDevice); } if (hasChanged) { log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " @@ -402,8 +398,12 @@ public class CsipDeviceManager { } private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) { - boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingEnabled(); + boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext); if (isAudioSharingEnabled) { + if (isWorkProfile()) { + log("addMemberDevicesIntoMainDevice: skip sync source for work profile"); + return; + } boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager) && BluetoothUtils.hasConnectedBroadcastSource( mainDevice, mBtManager); @@ -433,6 +433,8 @@ public class CsipDeviceManager { } } } + } else { + log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled"); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt index edd49c5a8fb7..0209eb8c3fbf 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt @@ -21,6 +21,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.os.DeadObjectException import android.os.IBinder import android.os.IInterface import android.os.RemoteException @@ -52,6 +53,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterIsInstance @@ -304,6 +306,14 @@ class DeviceSettingServiceConnection( service.registerDeviceSettingsListener(deviceInfo, listener) awaitClose { service.unregisterDeviceSettingsListener(deviceInfo, listener) } } + .catch { e -> + if (e is DeadObjectException) { + Log.e(TAG, "DeadObjectException happens when registering listener.", e) + emit(listOf()) + } else { + throw e + } + } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 0e060dfdd447..6d481dbe64e9 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -29,11 +29,13 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -48,6 +50,7 @@ import android.util.Pair; import com.android.internal.R; import com.android.settingslib.flags.Flags; +import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter; import com.android.settingslib.widget.AdaptiveIcon; import com.google.common.collect.ImmutableList; @@ -61,6 +64,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; import java.util.ArrayList; import java.util.Collections; @@ -69,6 +74,7 @@ import java.util.List; import java.util.Set; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class}) public class BluetoothUtilsTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) @@ -88,6 +94,7 @@ public class BluetoothUtilsTest { @Mock private BluetoothLeBroadcastReceiveState mLeBroadcastReceiveState; private Context mContext; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; private static final String STRING_METADATA = "string_metadata"; private static final String BOOL_METADATA = "true"; private static final String INT_METADATA = "25"; @@ -109,6 +116,7 @@ public class BluetoothUtilsTest { mContext = spy(RuntimeEnvironment.application); mSetFlagsRule.disableFlags(FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA); + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); @@ -1123,4 +1131,129 @@ public class BluetoothUtilsTest { AudioDeviceInfo.TYPE_HEARING_AID, address)); } + + @Test + public void isAudioSharingEnabled_flagOff_returnsFalse() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + + assertThat(BluetoothUtils.isAudioSharingEnabled()).isFalse(); + } + + @Test + public void isAudioSharingEnabled_featureNotSupported_returnsFalse() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_NOT_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + + assertThat(BluetoothUtils.isAudioSharingEnabled()).isFalse(); + } + + @Test + public void isAudioSharingEnabled_featureSupported_returnsTrue() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + + assertThat(BluetoothUtils.isAudioSharingEnabled()).isTrue(); + } + + @Test + public void isAudioSharingPreviewEnabled_flagOff_returnsFalse() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + + assertThat(BluetoothUtils.isAudioSharingPreviewEnabled( + mContext.getContentResolver())).isFalse(); + } + + @Test + public void isAudioSharingPreviewEnabled_featureNotSupported_returnsFalse() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_NOT_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + + assertThat(BluetoothUtils.isAudioSharingPreviewEnabled( + mContext.getContentResolver())).isFalse(); + } + + @Test + public void isAudioSharingPreviewEnabled_developerOptionOff_returnsFalse() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + Settings.Global.putInt(mContext.getContentResolver(), + BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 0); + + assertThat(BluetoothUtils.isAudioSharingPreviewEnabled( + mContext.getContentResolver())).isFalse(); + } + + @Test + public void isAudioSharingPreviewEnabled_developerOptionOn_returnsTrue() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + Settings.Global.putInt(mContext.getContentResolver(), + BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 1); + + assertThat(BluetoothUtils.isAudioSharingPreviewEnabled( + mContext.getContentResolver())).isTrue(); + } + + @Test + public void isAudioSharingUIAvailable_audioSharingAndPreviewFlagOff_returnsFalse() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + + assertThat(BluetoothUtils.isAudioSharingUIAvailable(mContext)).isFalse(); + } + + @Test + public void isAudioSharingUIAvailable_audioSharingAndPreviewDisabled_returnsFalse() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_NOT_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + + assertThat(BluetoothUtils.isAudioSharingUIAvailable(mContext)).isFalse(); + } + + @Test + public void isAudioSharingUIAvailable_audioSharingEnabled_returnsTrue() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + Settings.Global.putInt(mContext.getContentResolver(), + BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 0); + + assertThat(BluetoothUtils.isAudioSharingUIAvailable(mContext)).isTrue(); + } + + @Test + public void isAudioSharingUIAvailable_audioSharingPreviewEnabled_returnsTrue() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + Settings.Global.putInt(mContext.getContentResolver(), + BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 1); + + assertThat(BluetoothUtils.isAudioSharingUIAvailable(mContext)).isTrue(); + } } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 064198fc5e46..927a1c59cc76 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -284,5 +284,6 @@ public class SecureSettings { Settings.Secure.MANDATORY_BIOMETRICS, Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, Settings.Secure.ADVANCED_PROTECTION_MODE, + Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index c002a04d5b11..6d73ee27f076 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -332,6 +332,9 @@ public class SecureSettingsValidators { VALIDATORS.put( Secure.ACCESSIBILITY_QS_TARGETS, ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); + VALIDATORS.put( + Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS, + ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ONE_HANDED_MODE_ACTIVATED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ONE_HANDED_MODE_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 2034f36c558b..fb0aaf8e5ae1 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1823,6 +1823,9 @@ class SettingsProtoDumpUtil { Settings.Secure.ACCESSIBILITY_QS_TARGETS, SecureSettingsProto.Accessibility.QS_TARGETS); dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS, + SecureSettingsProto.Accessibility.ACCESSIBILITY_KEY_GESTURE_TARGETS); + dumpSetting(s, p, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, SecureSettingsProto.Accessibility.ACCESSIBILITY_MAGNIFICATION_CAPABILITY); dumpSetting(s, p, diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 24b91f9ab436..7b6321d1cc7d 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -959,6 +959,13 @@ <!-- Permission required for CTS test - CtsTelephonyTestCases --> <uses-permission android:name="android.permission.READ_BASIC_PHONE_STATE" /> + <!-- Permission required for ExecutableMethodFileOffsetsTest --> + <uses-permission android:name="android.permission.DYNAMIC_INSTRUMENTATION" /> + + <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest --> + <uses-permission android:name="android.permission.READ_SYSTEM_PREFERENCES" /> + <uses-permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index b4d81d6937ed..7c478ac78a29 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -353,7 +353,7 @@ public class BugreportProgressService extends Service { public void onDestroy() { mServiceHandler.getLooper().quit(); mScreenshotHandler.getLooper().quit(); - mBugreportSingleThreadExecutor.close(); + mBugreportSingleThreadExecutor.shutdown(); super.onDestroy(); } diff --git a/packages/SystemUI/README.md b/packages/SystemUI/README.md index 2910bba71341..635a97ef8a47 100644 --- a/packages/SystemUI/README.md +++ b/packages/SystemUI/README.md @@ -87,7 +87,7 @@ immediately for any callbacks added. There are a few places where CommandQueue is used as a bus to communicate across sysui. Such as when StatusBar calls CommandQueue#recomputeDisableFlags. -This is generally used a shortcut to directly trigger CommandQueue rather than +This is generally used as a shortcut to directly trigger CommandQueue rather than calling StatusManager and waiting for the call to come back to IStatusBar. ### [com.android.systemui.util.NotificationChannels](/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java) diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 5eae6d3e43fe..3df96030d221 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -16,6 +16,16 @@ flag { } flag { + name: "multiuser_wifi_picker_tracker_support" + namespace: "systemui" + description: "Adds WifiPickerTracker support for multiple users to support when HSUM is enabled." + bug: "371586248" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "udfps_view_performance" namespace: "systemui" description: "Decrease screen off blocking calls by waiting until the device is finished going to sleep before adding the udfps view." @@ -257,7 +267,7 @@ flag { flag { name: "dual_shade" namespace: "systemui" - description: "Enables the BC25 Dual Shade (go/bc25-dual-shade-design)." + description: "Enables Dual Shade (go/dual-shade-design-doc)." bug: "337259436" } @@ -631,9 +641,9 @@ flag { } flag { - name: "status_bar_simple_fragment" + name: "status_bar_root_modernization" namespace: "systemui" - description: "Feature flag for refactoring the collapsed status bar fragment" + description: "Feature flag for replacing the status bar fragment with a compose root" bug: "364360986" } @@ -1350,16 +1360,6 @@ flag { } flag { - name: "notification_pulsing_fix" - namespace: "systemui" - description: "Allow showing new pulsing notifications when the device is already pulsing." - bug: "335560575" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "media_lockscreen_launch_animation" namespace : "systemui" description : "Enable the origin launch animation for UMO when opening on top of lockscreen." @@ -1774,3 +1774,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "keyguard_transition_force_finish_on_screen_off" + namespace: "systemui" + description: "Forces KTF transitions to finish if the screen turns all the way off." + bug: "331636736" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt index eeab232542c0..163f4b36f472 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt @@ -59,6 +59,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset @@ -286,7 +287,10 @@ private fun DragHandle(dialog: Dialog) { Surface( modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) - .semantics { contentDescription = dragHandleContentDescription } + .semantics { + contentDescription = dragHandleContentDescription + hideFromAccessibility() + } .clickable { dialog.dismiss() }, color = MaterialTheme.colorScheme.onSurfaceVariant, shape = MaterialTheme.shapes.extraLarge, 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 e7b66c5f0d2f..d976e8e62f3c 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 @@ -693,8 +693,8 @@ private fun prepareInterruption( val fromState = updateStateInContent(transition.fromContent) val toState = updateStateInContent(transition.toContent) - reconcileStates(element, previousTransition) - reconcileStates(element, transition) + val previousUniqueState = reconcileStates(element, previousTransition, previousState = null) + reconcileStates(element, transition, previousState = previousUniqueState) // Remove the interruption values to all contents but the content(s) where the element will be // placed, to make sure that interruption deltas are computed only right after this interruption @@ -721,12 +721,32 @@ private fun prepareInterruption( /** * Reconcile the state of [element] in the formContent and toContent of [transition] so that the * values before interruption have their expected values, taking shared transitions into account. + * + * @return the unique state this element had during [transition], `null` if it had multiple + * different states (i.e. the shared animation was disabled). */ -private fun reconcileStates(element: Element, transition: TransitionState.Transition) { - val fromContentState = element.stateByContent[transition.fromContent] ?: return - val toContentState = element.stateByContent[transition.toContent] ?: return +private fun reconcileStates( + element: Element, + transition: TransitionState.Transition, + previousState: Element.State?, +): Element.State? { + fun reconcileWithPreviousState(state: Element.State) { + if (previousState != null && state.offsetBeforeInterruption == Offset.Unspecified) { + state.updateValuesBeforeInterruption(previousState) + } + } + + val fromContentState = element.stateByContent[transition.fromContent] + val toContentState = element.stateByContent[transition.toContent] + + if (fromContentState == null || toContentState == null) { + return (fromContentState ?: toContentState) + ?.also { reconcileWithPreviousState(it) } + ?.takeIf { it.offsetBeforeInterruption != Offset.Unspecified } + } + if (!isSharedElementEnabled(element.key, transition)) { - return + return null } if ( @@ -735,13 +755,19 @@ private fun reconcileStates(element: Element, transition: TransitionState.Transi ) { // Element is shared and placed in fromContent only. toContentState.updateValuesBeforeInterruption(fromContentState) - } else if ( + return fromContentState + } + + if ( toContentState.offsetBeforeInterruption != Offset.Unspecified && fromContentState.offsetBeforeInterruption == Offset.Unspecified ) { // Element is shared and placed in toContent only. fromContentState.updateValuesBeforeInterruption(toContentState) + return toContentState } + + return null } private fun Element.State.selfUpdateValuesBeforeInterruption() { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 4a9051598ddc..a301856d024f 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -2638,4 +2638,58 @@ class ElementTest { assertWithMessage("Frame $i didn't replace Foo").that(numberOfPlacements).isEqualTo(0) } } + + @Test + fun interruption_considerPreviousUniqueState() { + @Composable + fun SceneScope.Foo(modifier: Modifier = Modifier) { + Box(modifier.element(TestElements.Foo).size(50.dp)) + } + + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + scene(SceneC) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.offset(x = 100.dp, y = 100.dp)) } + } + } + } + + // During A => B, Foo disappears and stays in its original position. + scope.launch { state.startTransition(transition(SceneA, SceneB)) } + rule + .onNode(isElement(TestElements.Foo)) + .assertSizeIsEqualTo(50.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + + // Interrupt A => B by B => C. + var interruptionProgress by mutableFloatStateOf(1f) + scope.launch { + state.startTransition( + transition(SceneB, SceneC, interruptionProgress = { interruptionProgress }) + ) + } + + // During B => C, Foo appears again. It is still at (0, 0) when the interruption progress is + // 100%, and converges to its position (100, 100) in C. + rule + .onNode(isElement(TestElements.Foo)) + .assertSizeIsEqualTo(50.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + + interruptionProgress = 0.5f + rule + .onNode(isElement(TestElements.Foo)) + .assertSizeIsEqualTo(50.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + + interruptionProgress = 0f + rule + .onNode(isElement(TestElements.Foo)) + .assertSizeIsEqualTo(50.dp) + .assertPositionInRootIsEqualTo(100.dp, 100.dp) + } } diff --git a/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml new file mode 100644 index 000000000000..651e401681c6 --- /dev/null +++ b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <dimen name="keyguard_smartspace_top_offset">0dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/res/values-sw600dp/dimens.xml b/packages/SystemUI/customization/res/values-sw600dp/dimens.xml new file mode 100644 index 000000000000..10e630d44488 --- /dev/null +++ b/packages/SystemUI/customization/res/values-sw600dp/dimens.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <!-- For portrait direction in unfold foldable device, we don't need keyguard_smartspace_top_offset--> + <dimen name="keyguard_smartspace_top_offset">0dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/res/values/dimens.xml b/packages/SystemUI/customization/res/values/dimens.xml index c574d1fc674b..7feea6e5e8dd 100644 --- a/packages/SystemUI/customization/res/values/dimens.xml +++ b/packages/SystemUI/customization/res/values/dimens.xml @@ -33,4 +33,10 @@ <dimen name="small_clock_height">114dp</dimen> <dimen name="small_clock_padding_top">28dp</dimen> <dimen name="clock_padding_start">28dp</dimen> + + <!-- When large clock is showing, offset the smartspace by this amount --> + <dimen name="keyguard_smartspace_top_offset">12dp</dimen> + <!--Dimens used in both lockscreen preview and smartspace --> + <dimen name="date_weather_view_height">24dp</dimen> + <dimen name="enhanced_smartspace_height">104dp</dimen> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index a4782acaed9b..ee21ea6ee126 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -55,10 +55,7 @@ class FlexClockFaceController( override val view: View get() = layerController.view - override val config = - ClockFaceConfig( - hasCustomPositionUpdatedAnimation = false // TODO(b/364673982) - ) + override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true) override var theme = ThemeConfig(true, assets.seedColor) @@ -96,6 +93,19 @@ class FlexClockFaceController( layerController.view.layoutParams = lp } + /** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */ + private fun offsetGlyphsForStepClockAnimation( + clockStartLeft: Int, + direction: Int, + fraction: Float + ) { + (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation( + clockStartLeft, + direction, + fraction, + ) + } + override val layout: ClockFaceLayout = DefaultClockFaceLayout(view).apply { views[0].id = @@ -248,10 +258,12 @@ class FlexClockFaceController( override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { layerController.animations.onPositionUpdated(fromLeft, direction, fraction) + if (isLargeClock) offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) } override fun onPositionUpdated(distance: Float, fraction: Float) { layerController.animations.onPositionUpdated(distance, fraction) + // TODO(b/378128811) port stepping animation } } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index d86c0d664590..593eba9d05cc 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -19,6 +19,7 @@ package com.android.systemui.shared.clocks.view import android.content.Context import android.graphics.Canvas import android.graphics.Point +import android.util.MathUtils.constrainedMap import android.view.View import android.view.ViewGroup import android.widget.RelativeLayout @@ -50,6 +51,8 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me ) } + private val digitOffsets = mutableMapOf<Int, Float>() + override fun addView(child: View?) { super.addView(child) (child as SimpleDigitalClockTextView).digitTranslateAnimator = @@ -76,7 +79,7 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0) digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y) digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitSize) - digitLeftTopMap.forEach { _, point -> + digitLeftTopMap.forEach { (_, point) -> point.x += abs(aodTranslate.x) point.y += abs(aodTranslate.y) } @@ -89,11 +92,17 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - digitalClockTextViewMap.forEach { (id, _) -> - val textView = digitalClockTextViewMap[id]!! - canvas.translate(digitLeftTopMap[id]!!.x.toFloat(), digitLeftTopMap[id]!!.y.toFloat()) + digitalClockTextViewMap.forEach { (id, textView) -> + // save canvas location in anticipation of restoration later + canvas.save() + val xTranslateAmount = + digitOffsets.getOrDefault(id, 0f) + digitLeftTopMap[id]!!.x.toFloat() + // move canvas to location that the textView would like + canvas.translate(xTranslateAmount, digitLeftTopMap[id]!!.y.toFloat()) + // draw the textView at the location of the canvas above textView.draw(canvas) - canvas.translate(-digitLeftTopMap[id]!!.x.toFloat(), -digitLeftTopMap[id]!!.y.toFloat()) + // reset the canvas location back to 0 without drawing + canvas.restore() } } @@ -157,10 +166,108 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me } } + /** + * Offsets the textViews of the clock for the step clock animation. + * + * The animation makes the textViews of the clock move at different speeds, when the clock is + * moving horizontally. + * + * @param clockStartLeft the [getLeft] position of the clock, before it started moving. + * @param clockMoveDirection the direction in which it is moving. A positive number means right, + * and negative means left. + * @param moveFraction fraction of the clock movement. 0 means it is at the beginning, and 1 + * means it finished moving. + */ + fun offsetGlyphsForStepClockAnimation( + clockStartLeft: Int, + clockMoveDirection: Int, + moveFraction: Float, + ) { + val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0 + // The sign of moveAmountDeltaForDigit is already set here + // we can interpret (left - clockStartLeft) as (destinationPosition - originPosition) + // so we no longer need to multiply direct sign to moveAmountDeltaForDigit + val currentMoveAmount = left - clockStartLeft + for (i in 0 until NUM_DIGITS) { + val mapIndexToId = + when (i) { + 0 -> R.id.HOUR_FIRST_DIGIT + 1 -> R.id.HOUR_SECOND_DIGIT + 2 -> R.id.MINUTE_FIRST_DIGIT + 3 -> R.id.MINUTE_SECOND_DIGIT + else -> -1 + } + val digitFraction = + getDigitFraction( + digit = i, + isMovingToCenter = isMovingToCenter, + fraction = moveFraction, + ) + // left here is the final left position after the animation is done + val moveAmountForDigit = currentMoveAmount * digitFraction + var moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount + if (isMovingToCenter && moveAmountForDigit < 0) moveAmountDeltaForDigit *= -1 + digitOffsets[mapIndexToId] = moveAmountDeltaForDigit + invalidate() + } + } + + private val moveToCenterDelays: List<Int> + get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS + + private val moveToSideDelays: List<Int> + get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS + + private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float { + // The delay for the digit, in terms of fraction. + // (i.e. the digit should not move during 0.0 - 0.1). + val delays = if (isMovingToCenter) moveToCenterDelays else moveToSideDelays + val digitInitialDelay = delays[digit] * MOVE_DIGIT_STEP + return MOVE_INTERPOLATOR.getInterpolation( + constrainedMap( + /* rangeMin= */ 0.0f, + /* rangeMax= */ 1.0f, + /* valueMin= */ digitInitialDelay, + /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME, + /* value= */ fraction, + ) + ) + } + companion object { val AOD_TRANSITION_DURATION = 750L val CHARGING_TRANSITION_DURATION = 300L + // Calculate the positions of all of the digits... + // Offset each digit by, say, 0.1 + // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should + // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3 + // from 0.3 - 1.0. + private const val NUM_DIGITS = 4 + + // Delays. Each digit's animation should have a slight delay, so we get a nice + // "stepping" effect. When moving right, the second digit of the hour should move first. + // When moving left, the first digit of the hour should move first. The lists encode + // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied + // by delayMultiplier. + private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3) + private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2) + + // How much delay to apply to each subsequent digit. This is measured in terms of "fraction" + // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc + // before moving). + // + // The current specs dictate that each digit should have a 33ms gap between them. The + // overall time is 1s right now. + private const val MOVE_DIGIT_STEP = 0.033f + + // Constants for the animation + private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED + + // Total available transition time for each digit, taking into account the step. If step is + // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7. + private const val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1) + // Use the sign of targetTranslation to control the direction of digit translation fun updateDirectionalTargetTranslate(id: Int, targetTranslation: Point): Point { val outPoint = Point(targetTranslation) @@ -169,17 +276,14 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me outPoint.x *= -1 outPoint.y *= -1 } - R.id.HOUR_SECOND_DIGIT -> { outPoint.x *= 1 outPoint.y *= -1 } - R.id.MINUTE_FIRST_DIGIT -> { outPoint.x *= -1 outPoint.y *= 1 } - R.id.MINUTE_SECOND_DIGIT -> { outPoint.x *= 1 outPoint.y *= 1 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt index e1421691a92d..58fe2c9cbe57 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt @@ -36,6 +36,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 22 private const val OWNER_ID = 10 +private const val PASSWORD_ID = 30 private const val OPERATION_ID = 100L private const val MAX_ATTEMPTS = 5 @@ -247,7 +248,11 @@ class CredentialInteractorImplTest : SysuiTestCase() { private fun pinRequest(credentialOwner: Int = USER_ID): BiometricPromptRequest.Credential.Pin = BiometricPromptRequest.Credential.Pin( promptInfo(), - BiometricUserInfo(userId = USER_ID, deviceCredentialOwnerId = credentialOwner), + BiometricUserInfo( + userId = USER_ID, + deviceCredentialOwnerId = credentialOwner, + userIdForPasswordEntry = PASSWORD_ID, + ), BiometricOperationInfo(OPERATION_ID), ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt index 160865d625f5..f0d79bb83652 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.deviceentry.domain.interactor import android.content.pm.UserInfo +import android.os.PowerManager +import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils @@ -31,22 +33,26 @@ import com.android.systemui.flags.fakeSystemPropertiesHelper import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeTrustRepository -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AuthenticationFlags -import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.domain.interactor.selectedUserInteractor +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -228,6 +234,8 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { @Test fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep() = testScope.runTest { + setLockAfterScreenTimeout(0) + kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( @@ -243,35 +251,54 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { } @Test - fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleepInAod() = + fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_afterDelay() = testScope.runTest { + val delay = 5000 + setLockAfterScreenTimeout(delay) + kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) - assertThat(deviceUnlockStatus?.isUnlocked).isFalse() - kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, - testScope = this, + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) ) + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + kosmos.powerInteractor.setAsleepForTest() runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + advanceTimeBy(delay.toLong()) assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + } + + @Test + fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_powerButtonLocksInstantly() = + testScope.runTest { + setLockAfterScreenTimeout(5000) + kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = true + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( SuccessFingerprintAuthenticationStatus(0, true) ) runCurrent() assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON + ) + runCurrent() + + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() } @Test - fun deviceUnlockStatus_staysLocked_whenFingerprintUnlocked_whileDeviceAsleep() = + fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleep() = testScope.runTest { + setLockAfterScreenTimeout(0) val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) assertThat(deviceUnlockStatus?.isUnlocked).isFalse() - assertThat(kosmos.keyguardTransitionInteractor.getCurrentState()) - .isEqualTo(KeyguardState.LOCKSCREEN) kosmos.powerInteractor.setAsleepForTest() runCurrent() @@ -282,7 +309,7 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { SuccessFingerprintAuthenticationStatus(0, true) ) runCurrent() - assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() } @Test @@ -478,6 +505,98 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate) } + @Test + fun deviceUnlockStatus_locksImmediately_whenDreamStarts_noTimeout() = + testScope.runTest { + setLockAfterScreenTimeout(0) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + + startDreaming() + + assertThat(isUnlocked).isFalse() + } + + @Test + fun deviceUnlockStatus_locksWithDelay_afterDreamStarts_withTimeout() = + testScope.runTest { + val delay = 5000 + setLockAfterScreenTimeout(delay) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + + startDreaming() + assertThat(isUnlocked).isTrue() + + advanceTimeBy(delay - 1L) + assertThat(isUnlocked).isTrue() + + advanceTimeBy(1L) + assertThat(isUnlocked).isFalse() + } + + @Test + fun deviceUnlockStatus_doesNotLockWithDelay_whenDreamStopsBeforeTimeout() = + testScope.runTest { + val delay = 5000 + setLockAfterScreenTimeout(delay) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + + startDreaming() + assertThat(isUnlocked).isTrue() + + advanceTimeBy(delay - 1L) + assertThat(isUnlocked).isTrue() + + stopDreaming() + assertThat(isUnlocked).isTrue() + + advanceTimeBy(1L) + assertThat(isUnlocked).isTrue() + } + + @Test + fun deviceUnlockStatus_doesNotLock_whenDreamStarts_ifNotInteractive() = + testScope.runTest { + setLockAfterScreenTimeout(0) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + + startDreaming() + + assertThat(isUnlocked).isFalse() + } + + private fun TestScope.unlockDevice() { + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason") + runCurrent() + } + + private fun setLockAfterScreenTimeout(timeoutMs: Int) { + kosmos.fakeSettings.putIntForUser( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + timeoutMs, + kosmos.selectedUserInteractor.getSelectedUserId(), + ) + } + + private fun TestScope.startDreaming() { + kosmos.fakeKeyguardRepository.setDreaming(true) + runCurrent() + } + + private fun TestScope.stopDreaming() { + kosmos.fakeKeyguardRepository.setDreaming(false) + runCurrent() + } + private fun TestScope.verifyRestrictionReasonsForAuthFlags( vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?> ) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index bfe89de6229d..3d5498b61471 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -16,11 +16,14 @@ package com.android.systemui.keyguard.data.repository +import android.animation.Animator import android.animation.ValueAnimator +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest import com.android.app.animation.Interpolators +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.shared.model.KeyguardState @@ -41,6 +44,8 @@ import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import java.math.RoundingMode import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertTrue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.dropWhile @@ -53,6 +58,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) @@ -65,6 +71,8 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { private lateinit var underTest: KeyguardTransitionRepository private lateinit var runner: KeyguardTransitionRunner + private val animatorListener = mock<Animator.AnimatorListener>() + @Before fun setUp() { underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main) @@ -80,7 +88,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { runner.startTransition( this, TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()), - maxFrames = 100 + maxFrames = 100, ) assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN) @@ -107,7 +115,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { LOCKSCREEN, AOD, getAnimator(), - TransitionModeOnCanceled.LAST_VALUE + TransitionModeOnCanceled.LAST_VALUE, ), ) @@ -142,7 +150,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { LOCKSCREEN, AOD, getAnimator(), - TransitionModeOnCanceled.RESET + TransitionModeOnCanceled.RESET, ), ) @@ -177,7 +185,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { LOCKSCREEN, AOD, getAnimator(), - TransitionModeOnCanceled.REVERSE + TransitionModeOnCanceled.REVERSE, ), ) @@ -476,6 +484,49 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { assertThat(steps.size).isEqualTo(3) } + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF) + fun forceFinishCurrentTransition_noFurtherStepsEmitted() = + testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) + + var sentForceFinish = false + + runner.startTransition( + this, + TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()), + maxFrames = 100, + // Force-finish on the second frame. + frameCallback = { frameNumber -> + if (!sentForceFinish && frameNumber > 1) { + testScope.launch { underTest.forceFinishCurrentTransition() } + sentForceFinish = true + } + }, + ) + + val lastTwoRunningSteps = + steps.filter { it.transitionState == TransitionState.RUNNING }.takeLast(2) + + // Make sure we stopped emitting RUNNING steps early, but then emitted a final 1f step. + assertTrue(lastTwoRunningSteps[0].value < 0.5f) + assertTrue(lastTwoRunningSteps[1].value == 1f) + + assertEquals(steps.last().from, AOD) + assertEquals(steps.last().to, LOCKSCREEN) + assertEquals(steps.last().transitionState, TransitionState.FINISHED) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF) + fun forceFinishCurrentTransition_noTransitionStarted_noStepsEmitted() = + testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) + + underTest.forceFinishCurrentTransition() + assertEquals(0, steps.size) + } + private fun listWithStep( step: BigDecimal, start: BigDecimal = BigDecimal.ZERO, @@ -505,7 +556,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { to, fractions[0].toFloat(), TransitionState.STARTED, - OWNER_NAME + OWNER_NAME, ) ) fractions.forEachIndexed { index, fraction -> @@ -519,7 +570,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { to, fraction.toFloat(), TransitionState.RUNNING, - OWNER_NAME + OWNER_NAME, ) ) } @@ -538,6 +589,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) setDuration(10) + addListener(animatorListener) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt index 8914c80cdd5e..ae2a5c5fe501 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt @@ -34,6 +34,7 @@ package com.android.systemui.keyguard.domain.interactor +import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -43,6 +44,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -60,10 +62,13 @@ import com.android.systemui.scene.data.repository.setSceneTransition import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -96,7 +101,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, testScope = testScope, - throughTransitionState = TransitionState.RUNNING + throughTransitionState = TransitionState.RUNNING, ) powerInteractor.onCameraLaunchGestureDetected() @@ -134,7 +139,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN, testScope = testScope, - throughTransitionState = TransitionState.RUNNING + throughTransitionState = TransitionState.RUNNING, ) powerInteractor.onCameraLaunchGestureDetected() @@ -182,21 +187,12 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) runCurrent() - assertThat(values) - .containsExactly( - false, - true, - ) + assertThat(values).containsExactly(false, true) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false) runCurrent() - assertThat(values) - .containsExactly( - false, - true, - false, - ) + assertThat(values).containsExactly(false, true, false) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) runCurrent() @@ -228,7 +224,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, testScope = testScope, - throughTransitionState = TransitionState.RUNNING + throughTransitionState = TransitionState.RUNNING, ) powerInteractor.onCameraLaunchGestureDetected() @@ -242,10 +238,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { testScope = testScope, ) - assertThat(values) - .containsExactly( - false, - ) + assertThat(values).containsExactly(false) } @Test @@ -263,7 +256,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { from = KeyguardState.UNDEFINED, to = KeyguardState.AOD, testScope = testScope, - throughTransitionState = TransitionState.RUNNING + throughTransitionState = TransitionState.RUNNING, ) powerInteractor.onCameraLaunchGestureDetected() @@ -278,10 +271,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { testScope = testScope, ) - assertThat(values) - .containsExactly( - false, - ) + assertThat(values).containsExactly(false) } @Test @@ -304,8 +294,19 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { assertThat(occludingActivityWillDismissKeyguard).isTrue() // Re-lock device: - kosmos.powerInteractor.setAsleepForTest() - runCurrent() + lockDevice() assertThat(occludingActivityWillDismissKeyguard).isFalse() } + + private suspend fun TestScope.lockDevice() { + kosmos.powerInteractor.setAsleepForTest() + advanceTimeBy( + kosmos.userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index ecc62e908a4f..87ab3c89a671 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -69,7 +69,9 @@ class ClockSectionTest : SysuiTestCase() { get() = kosmos.fakeSystemBarUtilsProxy.getStatusBarHeight() + context.resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) + - context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + context.resources.getDimensionPixelSize( + customR.dimen.keyguard_smartspace_top_offset + ) private val LARGE_CLOCK_TOP get() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt index 1abb441439fe..5798e0776c4f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt @@ -21,6 +21,7 @@ import android.animation.ValueAnimator import android.view.Choreographer.FrameCallback import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.TransitionInfo +import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -35,9 +36,8 @@ import org.junit.Assert.fail * Gives direct control over ValueAnimator, in order to make transition tests deterministic. See * [AnimationHandler]. Animators are required to be run on the main thread, so dispatch accordingly. */ -class KeyguardTransitionRunner( - val repository: KeyguardTransitionRepository, -) : AnimationFrameCallbackProvider { +class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) : + AnimationFrameCallbackProvider { private var frameCount = 1L private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null)) @@ -48,7 +48,12 @@ class KeyguardTransitionRunner( * For transitions being directed by an animator. Will control the number of frames being * generated so the values are deterministic. */ - suspend fun startTransition(scope: CoroutineScope, info: TransitionInfo, maxFrames: Int = 100) { + suspend fun startTransition( + scope: CoroutineScope, + info: TransitionInfo, + maxFrames: Int = 100, + frameCallback: Consumer<Long>? = null, + ) { // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from main // thread withContext(Dispatchers.Main) { @@ -62,7 +67,12 @@ class KeyguardTransitionRunner( isTerminated = frameNumber >= maxFrames if (!isTerminated) { - withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) } + try { + withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) } + frameCallback?.accept(frameNumber) + } catch (e: IllegalStateException) { + e.printStackTrace() + } } } } @@ -90,9 +100,13 @@ class KeyguardTransitionRunner( override fun postFrameCallback(cb: FrameCallback) { frames.value = Pair(frameCount++, cb) } + override fun postCommitCallback(runnable: Runnable) {} + override fun getFrameTime() = frameCount + override fun getFrameDelay() = 1L + override fun setFrameDelay(delay: Long) {} companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt index 8e67e602abd9..f8f6fe246563 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.fakeSessionTokenFactory import com.android.systemui.res.R import com.android.systemui.testKosmos +import com.android.systemui.util.concurrency.execution import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runCurrent @@ -105,6 +106,7 @@ class Media3ActionFactoryTest : SysuiTestCase() { kosmos.looper, handler, kosmos.testScope, + kosmos.execution, ) controllerFactory.setMedia3Controller(media3Controller) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt index dda9cd5529a5..4dcbdfae6d4a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt @@ -104,67 +104,6 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { } } - @Test - fun showLabels_updatesFromSharedPreferences() = - with(kosmos) { - testScope.runTest { - val latest by collectLastValue(underTest.showLabels) - assertThat(latest).isFalse() - - setShowLabelsInSharedPreferences(true) - assertThat(latest).isTrue() - - setShowLabelsInSharedPreferences(false) - assertThat(latest).isFalse() - } - } - - @Test - fun showLabels_updatesFromUserChange() = - with(kosmos) { - testScope.runTest { - fakeUserRepository.setUserInfos(USERS) - val latest by collectLastValue(underTest.showLabels) - - fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) - setShowLabelsInSharedPreferences(false) - - fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) - setShowLabelsInSharedPreferences(true) - - fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) - assertThat(latest).isFalse() - } - } - - @Test - fun setShowLabels_inSharedPreferences() { - underTest.setShowLabels(false) - assertThat(getShowLabelsFromSharedPreferences(true)).isFalse() - - underTest.setShowLabels(true) - assertThat(getShowLabelsFromSharedPreferences(false)).isTrue() - } - - @Test - fun setShowLabels_forDifferentUser() = - with(kosmos) { - testScope.runTest { - fakeUserRepository.setUserInfos(USERS) - - fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) - underTest.setShowLabels(false) - assertThat(getShowLabelsFromSharedPreferences(true)).isFalse() - - fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) - underTest.setShowLabels(true) - assertThat(getShowLabelsFromSharedPreferences(false)).isTrue() - - fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) - assertThat(getShowLabelsFromSharedPreferences(true)).isFalse() - } - } - private fun getSharedPreferences(): SharedPreferences = with(kosmos) { return userFileManager.getSharedPreferences( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt index 3910903af4aa..ae7c44e9b146 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt @@ -35,7 +35,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class EditTileListStateTest : SysuiTestCase() { - private val underTest = EditTileListState(TestEditTiles, 4) + private val underTest = EditTileListState(TestEditTiles, columns = 4, largeTilesSpan = 2) @Test fun startDrag_listHasSpacers() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java index 0729e2fcd35f..03c1f92aad4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java @@ -93,6 +93,8 @@ public class QuickAccessWalletTileTest extends SysuiTestCase { private static final String CARD_DESCRIPTION = "•••• 1234"; private static final Icon CARD_IMAGE = Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888)); + private static final Icon INVALID_CARD_IMAGE = + Icon.createWithContentUri("content://media/external/images/media"); private static final int PRIMARY_USER_ID = 0; private static final int SECONDARY_USER_ID = 10; @@ -444,6 +446,14 @@ public class QuickAccessWalletTileTest extends SysuiTestCase { } @Test + public void testQueryCards_invalidDrawable_noSideViewDrawable() { + when(mKeyguardStateController.isUnlocked()).thenReturn(true); + setUpInvalidWalletCard(/* hasCard= */ true); + + assertNull(mTile.getState().sideViewCustomDrawable); + } + + @Test public void testQueryCards_error_notUpdateSideViewDrawable() { String errorMessage = "getWalletCardsError"; GetWalletCardsError error = new GetWalletCardsError(CARD_IMAGE, errorMessage); @@ -503,9 +513,33 @@ public class QuickAccessWalletTileTest extends SysuiTestCase { mTestableLooper.processAllMessages(); } + private void setUpInvalidWalletCard(boolean hasCard) { + GetWalletCardsResponse response = + new GetWalletCardsResponse( + hasCard + ? Collections.singletonList(createInvalidWalletCard(mContext)) + : Collections.EMPTY_LIST, 0); + + mTile.handleSetListening(true); + + verify(mController).queryWalletCards(mCallbackCaptor.capture()); + + mCallbackCaptor.getValue().onWalletCardsRetrieved(response); + mTestableLooper.processAllMessages(); + } + private WalletCard createWalletCard(Context context) { PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE); return new WalletCard.Builder(CARD_ID, CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build(); } + + private WalletCard createInvalidWalletCard(Context context) { + PendingIntent pendingIntent = + PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE); + return new WalletCard.Builder( + CARD_ID, INVALID_CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build(); + } + + } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 3be8a380b191..b5f005cdc706 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.scene +import android.provider.Settings import android.telephony.TelephonyManager import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -42,6 +43,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.ui.viewmodel.lockscreenUserActionsViewModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue @@ -64,6 +66,7 @@ import com.android.systemui.telephony.data.repository.fakeTelephonyRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository import com.android.telecom.mockTelecomManager import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage @@ -72,6 +75,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import org.junit.Before import org.junit.Test @@ -541,7 +545,14 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .isTrue() powerInteractor.setAsleepForTest() - testScope.runCurrent() + testScope.advanceTimeBy( + kosmos.userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + ) powerInteractor.setAwakeForTest() testScope.runCurrent() @@ -631,14 +642,25 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } /** Changes device wakefulness state from awake to asleep, going through intermediary states. */ - private fun Kosmos.putDeviceToSleep() { + private suspend fun Kosmos.putDeviceToSleep(waitForLock: Boolean = true) { val wakefulnessModel = powerInteractor.detailedWakefulness.value assertWithMessage("Cannot put device to sleep as it's already asleep!") .that(wakefulnessModel.isAwake()) .isTrue() powerInteractor.setAsleepForTest() - testScope.runCurrent() + if (waitForLock) { + testScope.advanceTimeBy( + kosmos.userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + ) + } else { + testScope.runCurrent() + } } /** Emulates the dismissal of the IME (soft keyboard). */ diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 55f88cc5b7a2..152911a30524 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -23,6 +23,7 @@ import android.hardware.face.FaceManager import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.provider.Settings import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -59,6 +60,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.haptics.vibratorHelper import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository +import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository @@ -106,6 +108,7 @@ import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvision import com.android.systemui.statusbar.sysuiStatusBarStateController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -753,7 +756,7 @@ class SceneContainerStartableTest : SysuiTestCase() { lastSleepReason = WakeSleepReason.POWER_BUTTON, powerButtonLaunchGestureTriggered = false, ) - transitionStateFlow.value = Transition(from = Scenes.Shade, to = Scenes.Lockscreen) + transitionStateFlow.value = Transition(from = Scenes.Gone, to = Scenes.Lockscreen) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) kosmos.fakePowerRepository.updateWakefulness( @@ -1339,7 +1342,14 @@ class SceneContainerStartableTest : SysuiTestCase() { // Putting the device to sleep to lock it again, which shouldn't report another // successful unlock. kosmos.powerInteractor.setAsleepForTest() - runCurrent() + advanceTimeBy( + kosmos.userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + ) // Verify that the startable changed the scene to Lockscreen because the device locked // following the sleep. assertThat(currentScene).isEqualTo(Scenes.Lockscreen) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt new file mode 100644 index 000000000000..a9a5cac6112e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.data.repository + +import android.view.Display +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.commandline.commandRegistry +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ShadePositionRepositoryTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val commandRegistry = kosmos.commandRegistry + private val pw = PrintWriter(StringWriter()) + + private val underTest = ShadePositionRepositoryImpl(commandRegistry) + + @Before + fun setUp() { + underTest.start() + } + + @Test + fun commandDisplayOverride_updatesDisplayId() = + testScope.runTest { + val displayId by collectLastValue(underTest.displayId) + assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) + + val newDisplayId = 2 + commandRegistry.onShellCommand( + pw, + arrayOf("shade_display_override", newDisplayId.toString()), + ) + + assertThat(displayId).isEqualTo(newDisplayId) + } + + @Test + fun commandShadeDisplayOverride_resetsDisplayId() = + testScope.runTest { + val displayId by collectLastValue(underTest.displayId) + assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) + + val newDisplayId = 2 + commandRegistry.onShellCommand( + pw, + arrayOf("shade_display_override", newDisplayId.toString()), + ) + assertThat(displayId).isEqualTo(newDisplayId) + + commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", "reset")) + assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt index 643acdbb9277..2a3878c17a1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt @@ -16,18 +16,22 @@ package com.android.systemui.statusbar.connectivity +import android.content.Context +import android.os.UserHandle import android.os.UserManager -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import androidx.lifecycle.Lifecycle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT import com.android.systemui.SysuiTestCase import com.android.systemui.settings.UserTracker -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.wifitrackerlib.WifiEntry import com.android.wifitrackerlib.WifiPickerTracker import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -35,36 +39,28 @@ import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyList import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.util.concurrent.Executor +import org.mockito.kotlin.any +import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) class AccessPointControllerImplTest : SysuiTestCase() { - @Mock - private lateinit var userManager: UserManager - @Mock - private lateinit var userTracker: UserTracker - @Mock - private lateinit var wifiPickerTrackerFactory: - WifiPickerTrackerFactory - @Mock - private lateinit var wifiPickerTracker: WifiPickerTracker - @Mock - private lateinit var callback: AccessPointController.AccessPointCallback - @Mock - private lateinit var otherCallback: AccessPointController.AccessPointCallback - @Mock - private lateinit var wifiEntryConnected: WifiEntry - @Mock - private lateinit var wifiEntryOther: WifiEntry - @Captor - private lateinit var wifiEntryListCaptor: ArgumentCaptor<List<WifiEntry>> + @Mock private lateinit var userManager: UserManager + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var wifiPickerTrackerFactory: WifiPickerTrackerFactory + @Mock private lateinit var wifiPickerTracker: WifiPickerTracker + @Mock private lateinit var callback: AccessPointController.AccessPointCallback + @Mock private lateinit var otherCallback: AccessPointController.AccessPointCallback + @Mock private lateinit var wifiEntryConnected: WifiEntry + @Mock private lateinit var wifiEntryOther: WifiEntry + @Captor private lateinit var wifiEntryListCaptor: ArgumentCaptor<List<WifiEntry>> private val instantExecutor = Executor { it.run() } private lateinit var controller: AccessPointControllerImpl @@ -72,19 +68,21 @@ class AccessPointControllerImplTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - `when`(wifiPickerTrackerFactory.create(any(), any(), any())).thenReturn(wifiPickerTracker) + `when`(wifiPickerTrackerFactory.create(any(), any(), any(), any())) + .thenReturn(wifiPickerTracker) `when`(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntryConnected) - `when`(wifiPickerTracker.wifiEntries).thenReturn(ArrayList<WifiEntry>().apply { - add(wifiEntryOther) - }) + `when`(wifiPickerTracker.wifiEntries) + .thenReturn(ArrayList<WifiEntry>().apply { add(wifiEntryOther) }) - controller = AccessPointControllerImpl( + controller = + AccessPointControllerImpl( + mContext, userManager, userTracker, instantExecutor, - wifiPickerTrackerFactory - ) + wifiPickerTrackerFactory, + ) controller.init() } @@ -183,13 +181,15 @@ class AccessPointControllerImplTest : SysuiTestCase() { @Test fun testReturnEmptyListWhenNoWifiPickerTracker() { - `when`(wifiPickerTrackerFactory.create(any(), any(), any())).thenReturn(null) - val otherController = AccessPointControllerImpl( + `when`(wifiPickerTrackerFactory.create(any(), any(), any(), any())).thenReturn(null) + val otherController = + AccessPointControllerImpl( + mContext, userManager, userTracker, instantExecutor, - wifiPickerTrackerFactory - ) + wifiPickerTrackerFactory, + ) otherController.init() otherController.addAccessPointCallback(callback) @@ -244,4 +244,19 @@ class AccessPointControllerImplTest : SysuiTestCase() { verify(wifiEntryOther).connect(any()) verify(callback, never()).onSettingsActivityTriggered(any()) } + + @Test + @EnableFlags(FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT) + fun switchUsers() { + val primaryUserMockContext = mock<Context>() + mContext.prepareCreateContextAsUser(UserHandle.of(PRIMARY_USER_ID), primaryUserMockContext) + controller.onUserSwitched(PRIMARY_USER_ID) + // Create is expected to be called once when the test starts and a second time when the user + // is switched. + verify(wifiPickerTrackerFactory, times(2)).create(any(), any(), any(), any()) + } + + private companion object { + private const val PRIMARY_USER_ID = 1 + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarInitializerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarInitializerTest.kt index 938da88a8819..009b33b9f808 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarInitializerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarInitializerTest.kt @@ -23,7 +23,6 @@ import android.platform.test.annotations.EnableFlags import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.kosmos.useUnconfinedTestDispatcher @@ -81,21 +80,21 @@ class StatusBarInitializerTest : SysuiTestCase() { ) @Test - @EnableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT) - fun simpleFragment_startsFromCoreStartable() { + @EnableFlags(StatusBarRootModernization.FLAG_NAME) + fun flagOn_startsFromCoreStartable() { underTest.start() assertThat(underTest.initialized).isTrue() } @Test - @EnableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT) - fun simpleFragment_throwsIfInitializeIsCalled() { + @EnableFlags(StatusBarRootModernization.FLAG_NAME) + fun flagOn_throwsIfInitializeIsCalled() { assertThrows(IllegalStateException::class.java) { underTest.initializeStatusBar() } } @Test - @EnableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT) - fun simpleFragment_flagEnabled_doesNotCreateFragment() { + @EnableFlags(StatusBarRootModernization.FLAG_NAME) + fun flagOn_flagEnabled_doesNotCreateFragment() { underTest.start() verify(fragmentManager, never()).beginTransaction() @@ -103,14 +102,14 @@ class StatusBarInitializerTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) fun flagOff_startCalled_stillInitializes() { underTest.start() assertThat(underTest.initialized).isTrue() } @Test - @DisableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) fun flagOff_doesNotThrowIfInitializeIsCalled() { underTest.initializeStatusBar() assertThat(underTest.initialized).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index ea5c29ef30aa..3ad41a54ac7e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator; +import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag; + import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertFalse; @@ -32,9 +34,9 @@ import static org.mockito.Mockito.when; import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.compose.animation.scene.ObservableTransitionState; @@ -42,6 +44,7 @@ import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.communal.shared.model.CommunalScenes; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.BrokenWithSceneContainer; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionState; @@ -78,14 +81,23 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.verification.VerificationMode; +import java.util.List; + import kotlinx.coroutines.flow.MutableStateFlow; import kotlinx.coroutines.test.TestScope; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper public class VisualStabilityCoordinatorTest extends SysuiTestCase { + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return parameterizeSceneContainerFlag(); + } + private VisualStabilityCoordinator mCoordinator; @Mock private DumpManager mDumpManager; @@ -117,6 +129,11 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { private NotificationEntry mEntry; private GroupEntry mGroupEntry; + public VisualStabilityCoordinatorTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -251,6 +268,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer public void testLockscreenPartlyShowing_groupAndSectionChangesNotAllowed() { // GIVEN the panel true expanded and device isn't pulsing setFullyDozed(false); @@ -267,6 +285,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer public void testLockscreenFullyShowing_groupAndSectionChangesNotAllowed() { // GIVEN the panel true expanded and device isn't pulsing setFullyDozed(false); @@ -520,6 +539,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { @Test @EnableFlags(Flags.FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION) + @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer public void testNotLockscreenInGoneTransition_invalidationCalled() { // GIVEN visual stability is being maintained b/c animation is playing mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava( @@ -589,6 +609,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer public void testCommunalShowingWillNotSuppressReordering() { // GIVEN panel is expanded, communal is showing, and QS is collapsed setPulsing(false); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt index dae5542123ed..50db9f7268e4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt @@ -21,16 +21,19 @@ import android.service.notification.StatusBarNotification import android.view.View.VISIBLE import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.domain.pipeline.MediaDataManager +import com.android.systemui.res.R import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.nullable @@ -52,8 +55,11 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var stackLayout: NotificationStackScrollLayout + @Mock private lateinit var seenNotificationsInteractor: SeenNotificationsInteractor private val testableResources = mContext.orCreateTestableResources + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var sizeCalculator: NotificationStackSizeCalculator @@ -72,7 +78,9 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { lockscreenShadeTransitionController = lockscreenShadeTransitionController, mediaDataManager = mediaDataManager, testableResources.resources, - ResourcesSplitShadeStateController() + ResourcesSplitShadeStateController(), + seenNotificationsInteractor = seenNotificationsInteractor, + scope = testScope, ) } @@ -85,7 +93,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { rows, spaceForNotifications = 0f, spaceForShelf = 0f, - shelfHeight = 0f + shelfHeight = 0f, ) assertThat(maxNotifications).isEqualTo(0) @@ -101,7 +109,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { rows, spaceForNotifications = Float.MAX_VALUE, spaceForShelf = Float.MAX_VALUE, - shelfHeight + shelfHeight, ) assertThat(maxNotifications).isEqualTo(numberOfRows) @@ -137,7 +145,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { listOf(row), /* spaceForNotifications= */ 5f, /* spaceForShelf= */ 0f, - /* shelfHeight= */ 0f + /* shelfHeight= */ 0f, ) assertThat(maxNotifications).isEqualTo(1) @@ -148,11 +156,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { setGapHeight(gapHeight) val shelfHeight = shelfHeight + dividerHeight val spaceForNotifications = - listOf( - rowHeight + dividerHeight, - gapHeight + rowHeight + dividerHeight, - ) - .sum() + listOf(rowHeight + dividerHeight, gapHeight + rowHeight + dividerHeight).sum() val spaceForShelf = gapHeight + dividerHeight + shelfHeight val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight)) @@ -162,7 +166,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { rows, spaceForNotifications + 1, spaceForShelf, - shelfHeight + shelfHeight, ) assertThat(maxNotifications).isEqualTo(2) @@ -173,12 +177,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { // Each row in separate section. setGapHeight(gapHeight) - val notifSpace = - listOf( - rowHeight, - dividerHeight + gapHeight + rowHeight, - ) - .sum() + val notifSpace = listOf(rowHeight, dividerHeight + gapHeight + rowHeight).sum() val shelfSpace = dividerHeight + gapHeight + shelfHeight val spaceUsed = notifSpace + shelfSpace @@ -209,7 +208,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { rows, spaceForNotifications + 1, spaceForShelf, - shelfHeight + shelfHeight, ) assertThat(maxNotifications).isEqualTo(1) @@ -252,7 +251,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { visibleIndex = 0, previousView = null, stack = stackLayout, - onLockscreen = true + onLockscreen = true, ) assertThat(space.whenEnoughSpace).isEqualTo(10f) } @@ -272,7 +271,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { visibleIndex = 0, previousView = null, stack = stackLayout, - onLockscreen = true + onLockscreen = true, ) assertThat(space.whenEnoughSpace).isEqualTo(5) } @@ -291,7 +290,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { visibleIndex = 0, previousView = null, stack = stackLayout, - onLockscreen = true + onLockscreen = true, ) assertThat(space.whenSavingSpace).isEqualTo(5) } @@ -311,7 +310,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { visibleIndex = 0, previousView = null, stack = stackLayout, - onLockscreen = true + onLockscreen = true, ) assertThat(space.whenSavingSpace).isEqualTo(5) } @@ -330,7 +329,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { visibleIndex = 0, previousView = null, stack = stackLayout, - onLockscreen = false + onLockscreen = false, ) assertThat(space.whenEnoughSpace).isEqualTo(rowHeight) assertThat(space.whenSavingSpace).isEqualTo(rowHeight) @@ -340,14 +339,14 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { rows: List<ExpandableView>, spaceForNotifications: Float, spaceForShelf: Float, - shelfHeight: Float = this.shelfHeight + shelfHeight: Float = this.shelfHeight, ): Int { setupChildren(rows) return sizeCalculator.computeMaxKeyguardNotifications( stackLayout, spaceForNotifications, spaceForShelf, - shelfHeight + shelfHeight, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index a8bcfbcfc539..39a1c106cfcf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.telephony.CellSignalStrength import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN @@ -735,9 +736,10 @@ class MobileIconInteractorTest : SysuiTestCase() { } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test // See b/346904529 for more context - fun satBasedIcon_doesNotInflateSignalStrength() = + fun satBasedIcon_doesNotInflateSignalStrength_flagOff() = testScope.runTest { val latest by collectLastValue(underTest.signalLevelIcon) @@ -756,7 +758,75 @@ class MobileIconInteractorTest : SysuiTestCase() { assertThat(latest!!.level).isEqualTo(4) } + @EnableFlags( + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG, + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN, + ) + @Test + // See b/346904529 for more context + fun satBasedIcon_doesNotInflateSignalStrength_flagOn() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // GIVEN a satellite connection + connectionRepository.isNonTerrestrial.value = true + // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH + connectionRepository.inflateSignalStrength.value = true + + connectionRepository.satelliteLevel.value = 4 + assertThat(latest!!.level).isEqualTo(4) + + connectionRepository.inflateSignalStrength.value = true + connectionRepository.primaryLevel.value = 4 + + // Icon level is unaffected + assertThat(latest!!.level).isEqualTo(4) + } + + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + @Test + fun satBasedIcon_usesPrimaryLevel_flagOff() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // GIVEN a satellite connection + connectionRepository.isNonTerrestrial.value = true + + // GIVEN primary level is set + connectionRepository.primaryLevel.value = 4 + connectionRepository.satelliteLevel.value = 0 + + // THEN icon uses the primary level because the flag is off + assertThat(latest!!.level).isEqualTo(4) + } + + @EnableFlags( + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG, + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN, + ) + @Test + fun satBasedIcon_usesSatelliteLevel_flagOn() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // GIVEN a satellite connection + connectionRepository.isNonTerrestrial.value = true + + // GIVEN satellite level is set + connectionRepository.satelliteLevel.value = 4 + connectionRepository.primaryLevel.value = 0 + + // THEN icon uses the satellite level because the flag is on + assertThat(latest!!.level).isEqualTo(4) + } + + /** + * Context (b/377518113), this test will not be needed after FLAG_CARRIER_ROAMING_NB_IOT_NTN is + * rolled out. The new API should report 0 automatically if not in service. + */ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test fun satBasedIcon_reportsLevelZeroWhenOutOfService() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 4c7cdfa7fb67..038722cd9608 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -151,7 +151,7 @@ class MobileIconViewModelTest : SysuiTestCase() { iconsInteractor.isForceHidden, repository, context, - MobileIconCarrierIdOverridesFake() + MobileIconCarrierIdOverridesFake(), ) createAndSetViewModel() } @@ -359,7 +359,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) connectionsRepository.mobileIsDefault.value = true repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) @@ -406,7 +406,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) repository.setDataEnabled(true) @@ -426,7 +426,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val initial = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) @@ -448,7 +448,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) repository.dataEnabled.value = true var latest: Icon? = null @@ -477,7 +477,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -499,7 +499,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -520,7 +520,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -542,7 +542,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( connectionsRepository.defaultMobileIconGroup.value.dataType, - ContentDescription.Resource(G.dataContentDescription) + ContentDescription.Resource(G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -564,7 +564,7 @@ class MobileIconViewModelTest : SysuiTestCase() { val expected = Icon.Resource( THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription) + ContentDescription.Resource(THREE_G.dataContentDescription), ) assertThat(latest).isEqualTo(expected) @@ -621,10 +621,7 @@ class MobileIconViewModelTest : SysuiTestCase() { underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this) repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = true, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = true) assertThat(inVisible).isFalse() assertThat(outVisible).isFalse() @@ -654,10 +651,7 @@ class MobileIconViewModelTest : SysuiTestCase() { underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = true, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = false) yield() @@ -666,20 +660,14 @@ class MobileIconViewModelTest : SysuiTestCase() { assertThat(containerVisible).isTrue() repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = true) assertThat(inVisible).isFalse() assertThat(outVisible).isTrue() assertThat(containerVisible).isTrue() repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = false) assertThat(inVisible).isFalse() assertThat(outVisible).isFalse() @@ -709,10 +697,7 @@ class MobileIconViewModelTest : SysuiTestCase() { underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = true, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = false) yield() @@ -721,20 +706,14 @@ class MobileIconViewModelTest : SysuiTestCase() { assertThat(containerVisible).isTrue() repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = true) assertThat(inVisible).isFalse() assertThat(outVisible).isTrue() assertThat(containerVisible).isTrue() repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = false) assertThat(inVisible).isFalse() assertThat(outVisible).isFalse() @@ -824,10 +803,7 @@ class MobileIconViewModelTest : SysuiTestCase() { // sets the background on cellular repository.hasPrioritizedNetworkCapabilities.value = true repository.dataActivityDirection.value = - DataActivityModel( - hasActivityIn = true, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = true) assertThat(roaming).isFalse() assertThat(networkTypeIcon).isNull() @@ -838,11 +814,13 @@ class MobileIconViewModelTest : SysuiTestCase() { } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun nonTerrestrial_usesSatelliteIcon() = + fun nonTerrestrial_usesSatelliteIcon_flagOff() = testScope.runTest { repository.isNonTerrestrial.value = true repository.setAllLevels(0) + repository.satelliteLevel.value = 0 val latest by collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) @@ -853,28 +831,66 @@ class MobileIconViewModelTest : SysuiTestCase() { // 1-2 -> 1 bar repository.setAllLevels(1) + repository.satelliteLevel.value = 1 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) repository.setAllLevels(2) + repository.satelliteLevel.value = 2 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) // 3-4 -> 2 bars repository.setAllLevels(3) + repository.satelliteLevel.value = 3 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) repository.setAllLevels(4) + repository.satelliteLevel.value = 4 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + + @EnableFlags( + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG, + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN, + ) + @Test + fun nonTerrestrial_usesSatelliteIcon_flagOn() = + testScope.runTest { + repository.isNonTerrestrial.value = true + repository.satelliteLevel.value = 0 + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.satelliteLevel.value = 1 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.satelliteLevel.value = 2 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.satelliteLevel.value = 3 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.satelliteLevel.value = 4 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun satelliteIcon_ignoresInflateSignalStrength() = + fun satelliteIcon_ignoresInflateSignalStrength_flagOff() = testScope.runTest { // Note that this is the exact same test as above, but with inflateSignalStrength set to // true we note that the level is unaffected by inflation repository.inflateSignalStrength.value = true repository.isNonTerrestrial.value = true repository.setAllLevels(0) + repository.satelliteLevel.value = 0 val latest by collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) @@ -885,16 +901,55 @@ class MobileIconViewModelTest : SysuiTestCase() { // 1-2 -> 1 bar repository.setAllLevels(1) + repository.satelliteLevel.value = 1 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) repository.setAllLevels(2) + repository.satelliteLevel.value = 2 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) // 3-4 -> 2 bars repository.setAllLevels(3) + repository.satelliteLevel.value = 3 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) repository.setAllLevels(4) + repository.satelliteLevel.value = 4 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + + @EnableFlags( + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG, + com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN, + ) + @Test + fun satelliteIcon_ignoresInflateSignalStrength_flagOn() = + testScope.runTest { + // Note that this is the exact same test as above, but with inflateSignalStrength set to + // true we note that the level is unaffected by inflation + repository.inflateSignalStrength.value = true + repository.isNonTerrestrial.value = true + repository.satelliteLevel.value = 0 + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.satelliteLevel.value = 1 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.satelliteLevel.value = 2 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.satelliteLevel.value = 3 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.satelliteLevel.value = 4 assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt index b5dbc3fe1b4d..33223aef11ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoMod import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl +import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.kotlinArgumentCaptor @@ -71,6 +72,7 @@ class WifiRepositorySwitcherTest : SysuiTestCase() { private val demoModelFlow = MutableStateFlow<FakeWifiEventModel?>(null) private val mainExecutor = FakeExecutor(FakeSystemClock()) + private val userRepository = FakeUserRepository() private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -82,10 +84,13 @@ class WifiRepositorySwitcherTest : SysuiTestCase() { // Never start in demo mode whenever(demoModeController.isInDemoMode).thenReturn(false) - whenever(wifiPickerTrackerFactory.create(any(), any(), any())).thenReturn(wifiPickerTracker) + whenever(wifiPickerTrackerFactory.create(any(), any(), any(), any())) + .thenReturn(wifiPickerTracker) realImpl = WifiRepositoryImpl( + mContext, + userRepository, testScope.backgroundScope, mainExecutor, testDispatcher, @@ -97,11 +102,7 @@ class WifiRepositorySwitcherTest : SysuiTestCase() { whenever(demoModeWifiDataSource.wifiEvents).thenReturn(demoModelFlow) - demoImpl = - DemoWifiRepository( - demoModeWifiDataSource, - testScope.backgroundScope, - ) + demoImpl = DemoWifiRepository(demoModeWifiDataSource, testScope.backgroundScope) underTest = WifiRepositorySwitcher( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 6b371d74eacc..8a4593032748 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.user.data.repository import android.app.admin.devicePolicyManager import android.content.pm.UserInfo +import android.internal.statusbar.fakeStatusBarService import android.os.UserHandle import android.os.UserManager import android.provider.Settings @@ -29,6 +30,7 @@ import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.res.R import com.android.systemui.settings.FakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.model.SelectedUserModel @@ -61,6 +63,7 @@ class UserRepositoryImplTest : SysuiTestCase() { private val globalSettings = kosmos.fakeGlobalSettings private val broadcastDispatcher = kosmos.broadcastDispatcher private val devicePolicyManager = kosmos.devicePolicyManager + private val statusBarService = kosmos.fakeStatusBarService @Mock private lateinit var manager: UserManager @@ -72,6 +75,10 @@ class UserRepositoryImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) tracker = FakeUserTracker() + context.orCreateTestableResources.addOverride( + R.bool.config_userSwitchingMustGoThroughLoginScreen, + false, + ) } @Test @@ -323,6 +330,8 @@ class UserRepositoryImplTest : SysuiTestCase() { tracker = tracker, broadcastDispatcher = broadcastDispatcher, devicePolicyManager = devicePolicyManager, + resources = context.resources, + statusBarService = statusBarService, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt index 26439df45ba3..f70b42638cda 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt @@ -49,35 +49,78 @@ class UserLogoutInteractorTest : SysuiTestCase() { @Before fun setUp() { userRepository.setUserInfos(USER_INFOS) - runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[1]) } + runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[2]) } + userRepository.setLogoutToSystemUserEnabled(false) + userRepository.setSecondaryUserLogoutEnabled(false) } @Test - fun logOut_doesNothing_whenAdminDisabledSecondaryLogout() { + fun logOut_doesNothing_whenBothLogoutOptionsAreDisabled() { testScope.runTest { val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) - val lastLogoutCount = userRepository.logOutSecondaryUserCallCount - userRepository.setSecondaryUserLogoutEnabled(false) + val secondaryUserLogoutCount = userRepository.logOutSecondaryUserCallCount + val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount assertThat(isLogoutEnabled).isFalse() underTest.logOut() + assertThat(userRepository.logOutSecondaryUserCallCount) + .isEqualTo(secondaryUserLogoutCount) + assertThat(userRepository.logOutToSystemUserCallCount) + .isEqualTo(logoutToSystemUserCount) + } + } + + @Test + fun logOut_logsOutSecondaryUser_whenAdminEnabledSecondaryLogout() { + testScope.runTest { + val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) + val lastLogoutCount = userRepository.logOutSecondaryUserCallCount + val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount + userRepository.setSecondaryUserLogoutEnabled(true) + assertThat(isLogoutEnabled).isTrue() + underTest.logOut() + assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount + 1) + assertThat(userRepository.logOutToSystemUserCallCount) + .isEqualTo(logoutToSystemUserCount) + } + } + + @Test + fun logOut_logsOutToSystemUser_whenLogoutToSystemUserIsEnabled() { + testScope.runTest { + val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) + val lastLogoutCount = userRepository.logOutSecondaryUserCallCount + val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount + userRepository.setLogoutToSystemUserEnabled(true) + assertThat(isLogoutEnabled).isTrue() + underTest.logOut() assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount) + assertThat(userRepository.logOutToSystemUserCallCount) + .isEqualTo(logoutToSystemUserCount + 1) } } @Test - fun logOut_logsOut_whenAdminEnabledSecondaryLogout() { + fun logOut_secondaryUserTakesPrecedence() { testScope.runTest { val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) val lastLogoutCount = userRepository.logOutSecondaryUserCallCount + val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount + userRepository.setLogoutToSystemUserEnabled(true) userRepository.setSecondaryUserLogoutEnabled(true) assertThat(isLogoutEnabled).isTrue() underTest.logOut() assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount + 1) + assertThat(userRepository.logOutToSystemUserCallCount) + .isEqualTo(logoutToSystemUserCount) } } companion object { private val USER_INFOS = - listOf(UserInfo(0, "System user", 0), UserInfo(10, "Regular user", 0)) + listOf( + UserInfo(0, "System user", 0), + UserInfo(10, "Regular user", 0), + UserInfo(11, "Secondary user", 0), + ) } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt index 7d55169e048a..89da46544f1e 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt @@ -13,11 +13,20 @@ */ package com.android.systemui.plugins.clocks +import android.content.Context import android.graphics.Rect import android.graphics.drawable.Drawable +import android.util.DisplayMetrics import android.view.View import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.BOTTOM +import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID +import androidx.constraintlayout.widget.ConstraintSet.START +import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.internal.annotations.Keep +import com.android.internal.policy.SystemBarUtils import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.Plugin import com.android.systemui.plugins.annotations.GeneratedImport @@ -149,7 +158,7 @@ interface ClockFaceLayout { @ProtectedReturn("return constraints;") /** Custom constraints to apply to preview ConstraintLayout. */ - fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet + fun applyPreviewConstraints(context: Context, constraints: ConstraintSet): ConstraintSet fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) } @@ -169,13 +178,84 @@ class DefaultClockFaceLayout(val view: View) : ClockFaceLayout { return constraints } - override fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet { - return constraints + override fun applyPreviewConstraints( + context: Context, + constraints: ConstraintSet, + ): ConstraintSet { + return applyDefaultPreviewConstraints(context, constraints) } override fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) { // Default clock doesn't need detailed control of view } + + companion object { + fun applyDefaultPreviewConstraints( + context: Context, + constraints: ConstraintSet, + ): ConstraintSet { + constraints.apply { + val lockscreenClockViewLargeId = getId(context, "lockscreen_clock_view_large") + constrainWidth(lockscreenClockViewLargeId, WRAP_CONTENT) + constrainHeight(lockscreenClockViewLargeId, WRAP_CONTENT) + constrainMaxHeight(lockscreenClockViewLargeId, 0) + + val largeClockTopMargin = + SystemBarUtils.getStatusBarHeight(context) + + getDimen(context, "small_clock_padding_top") + + getDimen(context, "keyguard_smartspace_top_offset") + + getDimen(context, "date_weather_view_height") + + getDimen(context, "enhanced_smartspace_height") + connect(lockscreenClockViewLargeId, TOP, PARENT_ID, TOP, largeClockTopMargin) + connect(lockscreenClockViewLargeId, START, PARENT_ID, START) + connect(lockscreenClockViewLargeId, END, PARENT_ID, END) + + // In preview, we'll show UDFPS icon for UDFPS devices + // and nothing for non-UDFPS devices, + // and we're not planning to add this vide in clockHostView + // so we only need position of device entry icon to constrain clock + // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection + val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom") + val defaultDensity = + DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() / + DisplayMetrics.DENSITY_DEFAULT.toFloat() + val lockIconRadiusPx = (defaultDensity * 36).toInt() + val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx + + connect(lockscreenClockViewLargeId, BOTTOM, PARENT_ID, BOTTOM, clockBottomMargin) + val smallClockViewId = getId(context, "lockscreen_clock_view") + constrainWidth(smallClockViewId, WRAP_CONTENT) + constrainHeight(smallClockViewId, getDimen(context, "small_clock_height")) + connect( + smallClockViewId, + START, + PARENT_ID, + START, + getDimen(context, "clock_padding_start") + + getDimen(context, "status_view_margin_horizontal"), + ) + val smallClockTopMargin = + getDimen(context, "keyguard_clock_top_margin") + + SystemBarUtils.getStatusBarHeight(context) + connect(smallClockViewId, TOP, PARENT_ID, TOP, smallClockTopMargin) + } + return constraints + } + + fun getId(context: Context, name: String): Int { + val packageName = context.packageName + val res = context.packageManager.getResourcesForApplication(packageName) + val id = res.getIdentifier(name, "id", packageName) + return id + } + + fun getDimen(context: Context, name: String): Int { + val packageName = context.packageName + val res = context.packageManager.getResourcesForApplication(packageName) + val id = res.getIdentifier(name, "dimen", packageName) + return if (id == 0) 0 else res.getDimensionPixelSize(id) + } + } } /** Events that should call when various rendering parameters change */ diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml index 2a27b47e54ca..4a53df9c2f29 100644 --- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml @@ -24,7 +24,6 @@ <!-- margin from keyguard status bar to clock. For split shade it should be keyguard_split_shade_top_margin - status_bar_header_height_keyguard = 8dp --> <dimen name="keyguard_clock_top_margin">8dp</dimen> - <dimen name="keyguard_smartspace_top_offset">0dp</dimen> <!-- QS--> <dimen name="qs_panel_padding_top">16dp</dimen> diff --git a/packages/SystemUI/res/values-sw600dp-port/config.xml b/packages/SystemUI/res/values-sw600dp-port/config.xml index f556b97eefc2..53d921b5e534 100644 --- a/packages/SystemUI/res/values-sw600dp-port/config.xml +++ b/packages/SystemUI/res/values-sw600dp-port/config.xml @@ -33,6 +33,9 @@ <!-- The number of columns in the infinite grid QuickSettings --> <integer name="quick_settings_infinite_grid_num_columns">6</integer> + <!-- The maximum width of large tiles in the infinite grid QuickSettings --> + <integer name="quick_settings_infinite_grid_tile_max_width">3</integer> + <integer name="power_menu_lite_max_columns">2</integer> <integer name="power_menu_lite_max_rows">3</integer> diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 393631e3364b..26f32ef60851 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -126,6 +126,4 @@ <dimen name="controls_content_padding">24dp</dimen> <dimen name="control_list_vertical_spacing">8dp</dimen> <dimen name="control_list_horizontal_spacing">16dp</dimen> - <!-- For portrait direction in unfold foldable device, we don't need keyguard_smartspace_top_offset--> - <dimen name="keyguard_smartspace_top_offset">0dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 82c8c44f1efe..48af82ad7943 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -79,6 +79,9 @@ <!-- The number of columns in the infinite grid QuickSettings --> <integer name="quick_settings_infinite_grid_num_columns">4</integer> + <!-- The maximum width of large tiles in the infinite grid QuickSettings --> + <integer name="quick_settings_infinite_grid_tile_max_width">4</integer> + <!-- The number of columns in the Dual Shade QuickSettings --> <integer name="quick_settings_dual_shade_num_columns">4</integer> @@ -1086,4 +1089,9 @@ enable the desktop specific features. --> <bool name="config_enableDesktopFeatureSet">false</bool> + + <!-- + Whether the user switching can only happen by logging out and going through the system user (login screen). + --> + <bool name="config_userSwitchingMustGoThroughLoginScreen">false</bool> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 7fa287944956..67eb5b0fdf6b 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -815,8 +815,7 @@ <dimen name="keyguard_clock_top_margin">18dp</dimen> <!-- The amount to shift the clocks during a small/large transition --> <dimen name="keyguard_clock_switch_y_shift">14dp</dimen> - <!-- When large clock is showing, offset the smartspace by this amount --> - <dimen name="keyguard_smartspace_top_offset">12dp</dimen> + <!-- The amount to translate lockscreen elements on the GONE->AOD transition --> <dimen name="keyguard_enter_from_top_translation_y">-100dp</dimen> <!-- The amount to translate lockscreen elements on the GONE->AOD transition, on device fold --> diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 24d619119983..df9f7053c3f3 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -35,6 +35,7 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.customization.R import com.android.systemui.dagger.qualifiers.Background @@ -62,6 +63,7 @@ import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.plugins.clocks.ZenData.ZenMode import com.android.systemui.res.R as SysuiR +import com.android.systemui.settings.UserTracker import com.android.systemui.shared.regionsampling.RegionSampler import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -80,7 +82,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import com.android.app.tracing.coroutines.launchTraced as launch /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by @@ -103,6 +104,7 @@ constructor( private val featureFlags: FeatureFlagsClassic, private val zenModeController: ZenModeController, private val zenModeInteractor: ZenModeInteractor, + private val userTracker: UserTracker, ) { var loggers = listOf( @@ -120,6 +122,10 @@ constructor( connectClock(value) } + private fun is24HourFormat(userId: Int? = null): Boolean { + return DateFormat.is24HourFormat(context, userId ?: userTracker.userId) + } + private fun disconnectClock(clock: ClockController?) { if (clock == null) { return @@ -186,7 +192,7 @@ constructor( var pastVisibility: Int? = null override fun onViewAttachedToWindow(view: View) { - clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) + clock.events.onTimeFormatChanged(is24HourFormat()) // Match the asing for view.parent's layout classes. smallClockFrame = (view.parent as ViewGroup)?.also { frame -> @@ -218,7 +224,7 @@ constructor( largeClockOnAttachStateChangeListener = object : OnAttachStateChangeListener { override fun onViewAttachedToWindow(p0: View) { - clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) + clock.events.onTimeFormatChanged(is24HourFormat()) } override fun onViewDetachedFromWindow(p0: View) {} @@ -358,7 +364,7 @@ constructor( } override fun onTimeFormatChanged(timeFormat: String?) { - clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) } + clock?.run { events.onTimeFormatChanged(is24HourFormat()) } } override fun onTimeZoneChanged(timeZone: TimeZone) { @@ -366,7 +372,7 @@ constructor( } override fun onUserSwitchComplete(userId: Int) { - clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) } + clock?.run { events.onTimeFormatChanged(is24HourFormat(userId)) } zenModeCallback.onNextAlarmChanged() } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 11dde6aa0dfb..71d4e9af6f55 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -147,7 +147,7 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_clock_switch_y_shift); mSmartspaceTopOffset = (int) (mContext.getResources().getDimensionPixelSize( - R.dimen.keyguard_smartspace_top_offset) + com.android.systemui.customization.R.dimen.keyguard_smartspace_top_offset) * mContext.getResources().getConfiguration().fontScale / mContext.getResources().getDisplayMetrics().density * SMARTSPACE_TOP_PADDING_MULTIPLIER); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 811b47d57c1d..a46b236d46fb 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -16,6 +16,7 @@ package com.android.systemui; +import android.animation.Animator; import android.annotation.SuppressLint; import android.app.ActivityThread; import android.app.Application; @@ -135,6 +136,9 @@ public class SystemUIApplication extends Application implements if (Flags.enableLayoutTracing()) { View.setTraceLayoutSteps(true); } + if (com.android.window.flags.Flags.systemUiPostAnimationEnd()) { + Animator.setPostNotifyEndListenerEnabled(true); + } if (mProcessWrapper.isSystemUser()) { IntentFilter bootCompletedFilter = new diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index c9c4fd594adc..6635d8b06a5d 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -22,6 +22,7 @@ import android.annotation.UserIdInt import android.app.admin.DevicePolicyManager import android.content.IntentFilter import android.os.UserHandle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockscreenCredential import com.android.keyguard.KeyguardSecurityModel @@ -57,7 +58,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext /** Defines interface for classes that can access authentication-related application state. */ @@ -178,6 +178,16 @@ interface AuthenticationRepository { * profile of an organization-owned device. */ @UserIdInt suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int + + /** + * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the + * device goes to sleep, this is the maximum time the device policy allows to wait before + * locking the device, despite what the user setting might be set to. + */ + suspend fun getMaximumTimeToLock(): Long + + /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */ + suspend fun getPowerButtonInstantlyLocks(): Boolean } @SysUISingleton @@ -324,6 +334,19 @@ constructor( } } + override suspend fun getMaximumTimeToLock(): Long { + return withContext(backgroundDispatcher) { + devicePolicyManager.getMaximumTimeToLock(/* admin= */ null, selectedUserId) + } + } + + /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */ + override suspend fun getPowerButtonInstantlyLocks(): Boolean { + return withContext(backgroundDispatcher) { + lockPatternUtils.getPowerButtonInstantlyLocks(selectedUserId) + } + } + private val selectedUserId: Int @UserIdInt get() = userRepository.getSelectedUserInfo().id diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 67579fd7f696..1c994731c393 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.authentication.domain.interactor import android.os.UserHandle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternView import com.android.internal.widget.LockscreenCredential @@ -49,7 +50,6 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** * Hosts application business logic related to user authentication. @@ -215,7 +215,7 @@ constructor( */ suspend fun authenticate( input: List<Any>, - tryAutoConfirm: Boolean = false + tryAutoConfirm: Boolean = false, ): AuthenticationResult { if (input.isEmpty()) { throw IllegalArgumentException("Input was empty!") @@ -254,6 +254,20 @@ constructor( return AuthenticationResult.FAILED } + /** + * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the + * device goes to sleep, this is the maximum time the device policy allows to wait before + * locking the device, despite what the user setting might be set to. + */ + suspend fun getMaximumTimeToLock(): Long { + return repository.getMaximumTimeToLock() + } + + /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */ + suspend fun getPowerButtonInstantlyLocks(): Boolean { + return !getAuthenticationMethod().isSecure || repository.getPowerButtonInstantlyLocks() + } + private suspend fun shouldSkipAuthenticationAttempt( authenticationMethod: AuthenticationMethodModel, isAutoConfirmAttempt: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt index f8254592819b..a107322423bb 100644 --- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -36,6 +36,7 @@ import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper import com.android.systemui.people.widget.PeopleBackupHelper +import com.android.systemui.qs.panels.domain.backup.QSPreferencesBackupHelper import com.android.systemui.res.R import com.android.systemui.settings.UserFileManagerImpl @@ -58,9 +59,9 @@ open class BackupHelper : BackupAgentHelper() { private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences" private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY = "systemui.keyguard.quickaffordance.shared_preferences" - private const val COMMUNAL_PREFS_BACKUP_KEY = - "systemui.communal.shared_preferences" + private const val COMMUNAL_PREFS_BACKUP_KEY = "systemui.communal.shared_preferences" private const val COMMUNAL_STATE_BACKUP_KEY = "systemui.communal_state" + private const val QS_PREFERENCES_BACKUP_KEY = "systemui.qs.shared_preferences" val controlsDataLock = Any() const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED" const val PERMISSION_SELF = "com.android.systemui.permission.SELF" @@ -74,22 +75,20 @@ open class BackupHelper : BackupAgentHelper() { val keys = PeopleBackupHelper.getFilesToBackup() addHelper( PEOPLE_TILES_BACKUP_KEY, - PeopleBackupHelper(this, userHandle, keys.toTypedArray()) + PeopleBackupHelper(this, userHandle, keys.toTypedArray()), ) addHelper( KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY, - KeyguardQuickAffordanceBackupHelper( - context = this, - userId = userHandle.identifier, - ), + KeyguardQuickAffordanceBackupHelper(context = this, userId = userHandle.identifier), + ) + addHelper( + QS_PREFERENCES_BACKUP_KEY, + QSPreferencesBackupHelper(context = this, userId = userHandle.identifier), ) if (communalEnabled()) { addHelper( COMMUNAL_PREFS_BACKUP_KEY, - CommunalPrefsBackupHelper( - context = this, - userId = userHandle.identifier, - ) + CommunalPrefsBackupHelper(context = this, userId = userHandle.identifier), ) addHelper( COMMUNAL_STATE_BACKUP_KEY, @@ -110,10 +109,7 @@ open class BackupHelper : BackupAgentHelper() { } private fun addControlsHelper(userId: Int) { - val file = UserFileManagerImpl.createFile( - userId = userId, - fileName = CONTROLS, - ) + val file = UserFileManagerImpl.createFile(userId = userId, fileName = CONTROLS) // The map in mapOf is guaranteed to be order preserving val controlsMap = mapOf(file.getPath() to getPPControlsFile(this, userId)) NoOverwriteFileBackupHelper(controlsDataLock, this, controlsMap).also { @@ -134,6 +130,7 @@ open class BackupHelper : BackupAgentHelper() { * @property lock a lock to hold while backing up and restoring the files. * @property context the context of the [BackupAgent] * @property fileNamesAndPostProcess a map from the filenames to back up and the post processing + * * ``` * actions to take * ``` @@ -141,7 +138,7 @@ open class BackupHelper : BackupAgentHelper() { private class NoOverwriteFileBackupHelper( val lock: Any, val context: Context, - val fileNamesAndPostProcess: Map<String, () -> Unit> + val fileNamesAndPostProcess: Map<String, () -> Unit>, ) : FileBackupHelper(context, *fileNamesAndPostProcess.keys.toTypedArray()) { override fun restoreEntity(data: BackupDataInputStream) { @@ -152,11 +149,12 @@ open class BackupHelper : BackupAgentHelper() { return } synchronized(lock) { - traceSection("File restore: ${data.key}") { - super.restoreEntity(data) - } - Log.d(TAG, "Finishing restore for ${data.key} for user ${context.userId}. " + - "Starting postProcess.") + traceSection("File restore: ${data.key}") { super.restoreEntity(data) } + Log.d( + TAG, + "Finishing restore for ${data.key} for user ${context.userId}. " + + "Starting postProcess.", + ) traceSection("Postprocess: ${data.key}") { fileNamesAndPostProcess.get(data.key)?.invoke() } @@ -167,7 +165,7 @@ open class BackupHelper : BackupAgentHelper() { override fun performBackup( oldState: ParcelFileDescriptor?, data: BackupDataOutput?, - newState: ParcelFileDescriptor? + newState: ParcelFileDescriptor?, ) { synchronized(lock) { super.performBackup(oldState, data, newState) } } @@ -176,15 +174,13 @@ open class BackupHelper : BackupAgentHelper() { private fun getPPControlsFile(context: Context, userId: Int): () -> Unit { return { - val file = UserFileManagerImpl.createFile( - userId = userId, - fileName = BackupHelper.CONTROLS, - ) + val file = UserFileManagerImpl.createFile(userId = userId, fileName = BackupHelper.CONTROLS) if (file.exists()) { - val dest = UserFileManagerImpl.createFile( - userId = userId, - fileName = AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME, - ) + val dest = + UserFileManagerImpl.createFile( + userId = userId, + fileName = AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME, + ) file.copyTo(dest) val jobScheduler = context.getSystemService(JobScheduler::class.java) jobScheduler?.schedule( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt index b07006887011..08b3e99fadd0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt @@ -72,7 +72,7 @@ constructor( // Request LockSettingsService to return the Gatekeeper Password in the // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the // Gatekeeper Password and operationId. - var effectiveUserId = request.userInfo.userIdForPasswordEntry + var effectiveUserId = request.userInfo.deviceCredentialOwnerId val response = if (Flags.privateSpaceBp() && effectiveUserId != request.userInfo.userId) { effectiveUserId = request.userInfo.userId diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt index c464a66ea0c3..6c335e71cfde 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt @@ -18,13 +18,18 @@ package com.android.systemui.deviceentry import com.android.keyguard.EmptyLockIconViewController import com.android.keyguard.LockIconViewController +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.data.repository.DeviceEntryRepositoryModule import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigModule +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import dagger.Binds import dagger.Lazy import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import dagger.multibindings.Multibinds @Module(includes = [DeviceEntryRepositoryModule::class, FaceWakeUpTriggersConfigModule::class]) @@ -34,6 +39,13 @@ abstract class DeviceEntryModule { */ @Multibinds abstract fun deviceEntryIconTransitionSet(): Set<DeviceEntryIconTransition> + @Binds + @IntoMap + @ClassKey(DeviceUnlockedInteractor.Activator::class) + abstract fun deviceUnlockedInteractorActivator( + activator: DeviceUnlockedInteractor.Activator + ): CoreStartable + companion object { @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt index 3f937bba46d4..675f00a89d23 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt @@ -5,6 +5,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.user.data.repository.UserRepository import dagger.Binds @@ -42,6 +43,8 @@ interface DeviceEntryRepository { */ val isBypassEnabled: StateFlow<Boolean> + val deviceUnlockStatus: MutableStateFlow<DeviceUnlockStatus> + /** * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has * chosen any secure authentication method and even if they set the lockscreen to be dismissed @@ -84,6 +87,9 @@ constructor( initialValue = keyguardBypassController.bypassEnabled, ) + override val deviceUnlockStatus = + MutableStateFlow(DeviceUnlockStatus(isUnlocked = false, deviceUnlockSource = null)) + override suspend fun isLockscreenEnabled(): Boolean { return withContext(backgroundDispatcher) { val selectedUserId = userRepository.getSelectedUserInfo().id diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt index 5259c5dca39f..b74ca035a229 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt @@ -16,7 +16,10 @@ package com.android.systemui.deviceentry.domain.interactor +import android.provider.Settings +import android.util.Log import androidx.annotation.VisibleForTesting +import com.android.systemui.CoreStartable import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.dagger.SysUISingleton @@ -26,42 +29,51 @@ import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReaso import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus import com.android.systemui.flags.SystemPropertiesHelper -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.TrustInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class DeviceUnlockedInteractor @Inject constructor( - @Application private val applicationScope: CoroutineScope, - authenticationInteractor: AuthenticationInteractor, - deviceEntryRepository: DeviceEntryRepository, + private val authenticationInteractor: AuthenticationInteractor, + private val repository: DeviceEntryRepository, trustInteractor: TrustInteractor, faceAuthInteractor: DeviceEntryFaceAuthInteractor, fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, private val powerInteractor: PowerInteractor, private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, private val systemPropertiesHelper: SystemPropertiesHelper, - keyguardTransitionInteractor: KeyguardTransitionInteractor, -) { + private val userAwareSecureSettingsRepository: UserAwareSecureSettingsRepository, + private val keyguardInteractor: KeyguardInteractor, +) : ExclusiveActivatable() { private val deviceUnlockSource = merge( @@ -69,7 +81,7 @@ constructor( faceAuthInteractor.isAuthenticated .filter { it } .map { - if (deviceEntryRepository.isBypassEnabled.value) { + if (repository.isBypassEnabled.value) { DeviceUnlockSource.FaceWithBypass } else { DeviceUnlockSource.FaceWithoutBypass @@ -163,43 +175,160 @@ constructor( * proceed. */ val deviceUnlockStatus: StateFlow<DeviceUnlockStatus> = - authenticationInteractor.authenticationMethod - .flatMapLatest { authMethod -> - if (!authMethod.isSecure) { - flowOf(DeviceUnlockStatus(true, null)) - } else if (authMethod == AuthenticationMethodModel.Sim) { - // Device is locked if SIM is locked. - flowOf(DeviceUnlockStatus(false, null)) - } else { - combine( - powerInteractor.isAsleep, - isInLockdown, - keyguardTransitionInteractor - .transitionValue(KeyguardState.AOD) - .map { it == 1f } - .distinctUntilChanged(), - ::Triple, - ) - .flatMapLatestConflated { (isAsleep, isInLockdown, isAod) -> - val isForceLocked = - when { - isAsleep && !isAod -> true - isInLockdown -> true - else -> false - } - if (isForceLocked) { - flowOf(DeviceUnlockStatus(false, null)) + repository.deviceUnlockStatus.asStateFlow() + + override suspend fun onActivated(): Nothing { + authenticationInteractor.authenticationMethod.collectLatest { authMethod -> + if (!authMethod.isSecure) { + // Device remains unlocked as long as the authentication method is not secure. + Log.d(TAG, "remaining unlocked because auth method not secure") + repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, null) + } else if (authMethod == AuthenticationMethodModel.Sim) { + // Device remains locked while SIM is locked. + Log.d(TAG, "remaining locked because SIM locked") + repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null) + } else { + handleLockAndUnlockEvents() + } + } + + awaitCancellation() + } + + private suspend fun handleLockAndUnlockEvents() { + try { + Log.d(TAG, "started watching for lock and unlock events") + coroutineScope { + launch { handleUnlockEvents() } + launch { handleLockEvents() } + } + } finally { + Log.d(TAG, "stopped watching for lock and unlock events") + } + } + + private suspend fun handleUnlockEvents() { + // Unlock the device when a new unlock source is detected. + deviceUnlockSource.collect { + Log.d(TAG, "unlocking due to \"$it\"") + repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, it) + } + } + + private suspend fun handleLockEvents() { + merge( + // Device wakefulness events. + powerInteractor.detailedWakefulness + .map { Pair(it.isAsleep(), it.lastSleepReason) } + .distinctUntilChangedBy { it.first } + .map { (isAsleep, lastSleepReason) -> + if (isAsleep) { + if ( + lastSleepReason == WakeSleepReason.POWER_BUTTON && + authenticationInteractor.getPowerButtonInstantlyLocks() + ) { + LockImmediately("locked instantly from power button") + } else { + LockWithDelay("entering sleep") + } + } else { + CancelDelayedLock("waking up") + } + }, + // Device enters lockdown. + isInLockdown + .distinctUntilChanged() + .filter { it } + .map { LockImmediately("lockdown") }, + // Started dreaming + powerInteractor.isInteractive.flatMapLatestConflated { isInteractive -> + // Only respond to dream state changes while the device is interactive. + if (isInteractive) { + keyguardInteractor.isDreamingAny.distinctUntilChanged().map { isDreaming -> + if (isDreaming) { + LockWithDelay("started dreaming") } else { - deviceUnlockSource.map { DeviceUnlockStatus(true, it) } + CancelDelayedLock("stopped dreaming") } } + } else { + emptyFlow() + } + }, + ) + .collectLatest(::onLockEvent) + } + + private suspend fun onLockEvent(event: LockEvent) { + val debugReason = event.debugReason + when (event) { + is LockImmediately -> { + Log.d(TAG, "locking without delay due to \"$debugReason\"") + repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null) + } + + is LockWithDelay -> { + val lockDelay = lockDelay() + Log.d(TAG, "locking in ${lockDelay}ms due to \"$debugReason\"") + try { + delay(lockDelay) + Log.d( + TAG, + "locking after having waited for ${lockDelay}ms due to \"$debugReason\"", + ) + repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null) + } catch (_: CancellationException) { + Log.d( + TAG, + "delayed locking canceled, original delay was ${lockDelay}ms and reason was \"$debugReason\"", + ) } } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = DeviceUnlockStatus(false, null), - ) + + is CancelDelayedLock -> { + // Do nothing, the mere receipt of this inside of a "latest" block means that any + // previous coroutine is automatically canceled. + } + } + } + + /** + * Returns the amount of time to wait before locking down the device after the device has been + * put to sleep by the user, in milliseconds. + */ + private suspend fun lockDelay(): Long { + val lockAfterScreenTimeoutSetting = + userAwareSecureSettingsRepository + .getInt( + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, + ) + .toLong() + Log.d(TAG, "Lock after screen timeout setting set to ${lockAfterScreenTimeoutSetting}ms") + + val maxTimeToLockDevicePolicy = authenticationInteractor.getMaximumTimeToLock() + Log.d(TAG, "Device policy max set to ${maxTimeToLockDevicePolicy}ms") + + if (maxTimeToLockDevicePolicy <= 0) { + // No device policy enforced maximum. + Log.d(TAG, "No device policy max, delay is ${lockAfterScreenTimeoutSetting}ms") + return lockAfterScreenTimeoutSetting + } + + val screenOffTimeoutSetting = + userAwareSecureSettingsRepository + .getInt( + Settings.System.SCREEN_OFF_TIMEOUT, + KeyguardViewMediator.KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT, + ) + .coerceAtLeast(0) + .toLong() + Log.d(TAG, "Screen off timeout setting set to ${screenOffTimeoutSetting}ms") + + return (maxTimeToLockDevicePolicy - screenOffTimeoutSetting) + .coerceIn(minimumValue = 0, maximumValue = lockAfterScreenTimeoutSetting) + .also { Log.d(TAG, "Device policy max enforced, delay is ${it}ms") } + } private fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean { return when (this) { @@ -226,7 +355,30 @@ constructor( return systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE } + /** [CoreStartable] that activates the [DeviceUnlockedInteractor]. */ + class Activator + @Inject + constructor( + @Application private val applicationScope: CoroutineScope, + private val interactor: DeviceUnlockedInteractor, + ) : CoreStartable { + override fun start() { + applicationScope.launch { interactor.activate() } + } + } + + private sealed interface LockEvent { + val debugReason: String + } + + private data class LockImmediately(override val debugReason: String) : LockEvent + + private data class LockWithDelay(override val debugReason: String) : LockEvent + + private data class CancelDelayedLock(override val debugReason: String) : LockEvent + companion object { + private val TAG = "DeviceUnlockedInteractor" @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last" @VisibleForTesting const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update" } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index dd08d3262546..7a95a41770ac 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -40,7 +40,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.Flags; import com.android.systemui.biometrics.AuthController; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dock.DockManager; @@ -566,8 +565,7 @@ public class DozeTriggers implements DozeMachine.Part { } // When already in pulsing, we can show the new Notification without requesting a new pulse. - if (Flags.notificationPulsingFix() - && dozeState == State.DOZE_PULSING && reason == DozeLog.PULSE_REASON_NOTIFICATION) { + if (dozeState == State.DOZE_PULSING && reason == DozeLog.PULSE_REASON_NOTIFICATION) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 60a306b3e245..2ee9ddb0e453 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -239,7 +239,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private static final boolean ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS = Flags.ensureKeyguardDoesTransitionStarting(); - private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; + public static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000; private static final boolean DEBUG = KeyguardConstants.DEBUG; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 3a5614fbc430..eaf8fa9585f6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -114,6 +114,18 @@ interface KeyguardTransitionRepository { @FloatRange(from = 0.0, to = 1.0) value: Float, state: TransitionState, ) + + /** + * Forces the current transition to emit FINISHED, foregoing any additional RUNNING steps that + * otherwise would have been emitted. + * + * When the screen is off, upcoming performance changes cause all Animators to cease emitting + * frames, which means the Animator passed to [startTransition] will never finish if it was + * running when the screen turned off. Also, there's simply no reason to emit RUNNING steps when + * the screen isn't even on. As long as we emit FINISHED, everything should end up in the + * correct state. + */ + suspend fun forceFinishCurrentTransition() } @SysUISingleton @@ -134,6 +146,7 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR override val transitions = _transitions.asSharedFlow().distinctUntilChanged() private var lastStep: TransitionStep = TransitionStep() private var lastAnimator: ValueAnimator? = null + private var animatorListener: AnimatorListenerAdapter? = null private val withContextMutex = Mutex() private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> = @@ -233,7 +246,7 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR ) } - val adapter = + animatorListener = object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { emitTransition( @@ -254,9 +267,10 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR animator.removeListener(this) animator.removeUpdateListener(updateListener) lastAnimator = null + animatorListener = null } } - animator.addListener(adapter) + animator.addListener(animatorListener) animator.addUpdateListener(updateListener) animator.start() return@withContext null @@ -290,6 +304,33 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR } } + override suspend fun forceFinishCurrentTransition() { + withContextMutex.lock() + + if (lastAnimator?.isRunning != true) { + return + } + + return withContext("$TAG#forceFinishCurrentTransition", mainDispatcher) { + withContextMutex.unlock() + + Log.d(TAG, "forceFinishCurrentTransition() - emitting FINISHED early.") + + lastAnimator?.apply { + // Cancel the animator, but remove listeners first so we don't emit CANCELED. + removeAllListeners() + cancel() + + // Emit a final 1f RUNNING step to ensure that any transitions not listening for a + // FINISHED step end up in the right end state. + emitTransition(TransitionStep(currentTransitionInfo, 1f, TransitionState.RUNNING)) + + // Ask the listener to emit FINISHED and clean up its state. + animatorListener?.onAnimationEnd(this) + } + } + } + private suspend fun updateTransitionInternal( transitionId: UUID, @FloatRange(from = 0.0, to = 1.0) value: Float, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index b815f1988e7e..7cd2744cb7dc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -19,8 +19,10 @@ package com.android.systemui.keyguard.domain.interactor import android.annotation.SuppressLint import android.util.Log +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey +import com.android.systemui.Flags.keyguardTransitionForceFinishOnScreenOff import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository @@ -30,6 +32,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes @@ -59,7 +63,6 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** Encapsulates business-logic related to the keyguard transitions. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -70,6 +73,7 @@ constructor( @Application val scope: CoroutineScope, private val repository: KeyguardTransitionRepository, private val sceneInteractor: SceneInteractor, + private val powerInteractor: PowerInteractor, ) { private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>() @@ -188,6 +192,18 @@ constructor( } } } + + if (keyguardTransitionForceFinishOnScreenOff()) { + /** + * If the screen is turning off, finish the current transition immediately. Further + * frames won't be visible anyway. + */ + scope.launch { + powerInteractor.screenPowerState + .filter { it == ScreenPowerState.SCREEN_TURNING_OFF } + .collect { repository.forceFinishCurrentTransition() } + } + } } fun transition(edge: Edge, edgeWithoutSceneContainer: Edge? = null): Flow<TransitionStep> { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt index 46f5c05092eb..914fdd20e48e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt @@ -18,34 +18,23 @@ package com.android.systemui.keyguard.ui.binder import android.content.Context -import android.util.DisplayMetrics import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.ConstraintSet.BOTTOM -import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import androidx.constraintlayout.widget.ConstraintSet.START -import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.internal.policy.SystemBarUtils -import com.android.systemui.customization.R as customR import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer -import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection.Companion.getDimen import com.android.systemui.keyguard.ui.view.layout.sections.setVisibility import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.clocks.ClockController -import com.android.systemui.res.R import com.android.systemui.shared.clocks.ClockRegistry -import com.android.systemui.util.Utils import kotlin.reflect.KSuspendFunction1 /** Binder for the small clock view, large clock view. */ @@ -131,78 +120,6 @@ object KeyguardPreviewClockViewBinder { } } - private fun applyClockDefaultConstraints(context: Context, constraints: ConstraintSet) { - constraints.apply { - constrainWidth(customR.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT) - // The following two lines make lockscreen_clock_view_large is constrained to available - // height when it goes beyond constraints; otherwise, it use WRAP_CONTENT - constrainHeight(customR.id.lockscreen_clock_view_large, WRAP_CONTENT) - constrainMaxHeight(customR.id.lockscreen_clock_view_large, 0) - val largeClockTopMargin = - SystemBarUtils.getStatusBarHeight(context) + - context.resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) + - context.resources.getDimensionPixelSize( - R.dimen.keyguard_smartspace_top_offset - ) + - getDimen(context, DATE_WEATHER_VIEW_HEIGHT) + - getDimen(context, ENHANCED_SMARTSPACE_HEIGHT) - connect( - customR.id.lockscreen_clock_view_large, - TOP, - PARENT_ID, - TOP, - largeClockTopMargin, - ) - connect(customR.id.lockscreen_clock_view_large, START, PARENT_ID, START) - connect( - customR.id.lockscreen_clock_view_large, - ConstraintSet.END, - PARENT_ID, - ConstraintSet.END, - ) - - // In preview, we'll show UDFPS icon for UDFPS devices and nothing for non-UDFPS - // devices, but we need position of device entry icon to constrain clock - if (getConstraint(lockId) != null) { - connect(customR.id.lockscreen_clock_view_large, BOTTOM, lockId, TOP) - } else { - // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection - val bottomPaddingPx = - context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom) - val defaultDensity = - DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() / - DisplayMetrics.DENSITY_DEFAULT.toFloat() - val lockIconRadiusPx = (defaultDensity * 36).toInt() - val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx - connect( - customR.id.lockscreen_clock_view_large, - BOTTOM, - PARENT_ID, - BOTTOM, - clockBottomMargin, - ) - } - - constrainWidth(customR.id.lockscreen_clock_view, WRAP_CONTENT) - constrainHeight( - customR.id.lockscreen_clock_view, - context.resources.getDimensionPixelSize(customR.dimen.small_clock_height), - ) - connect( - customR.id.lockscreen_clock_view, - START, - PARENT_ID, - START, - context.resources.getDimensionPixelSize(customR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal), - ) - val smallClockTopMargin = - context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + - Utils.getStatusBarHeaderHeightKeyguard(context) - connect(customR.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin) - } - } - private fun applyPreviewConstraints( context: Context, rootView: ConstraintLayout, @@ -210,9 +127,8 @@ object KeyguardPreviewClockViewBinder { viewModel: KeyguardPreviewClockViewModel, ) { val cs = ConstraintSet().apply { clone(rootView) } - applyClockDefaultConstraints(context, cs) - previewClock.largeClock.layout.applyPreviewConstraints(cs) - previewClock.smallClock.layout.applyPreviewConstraints(cs) + previewClock.largeClock.layout.applyPreviewConstraints(context, cs) + previewClock.smallClock.layout.applyPreviewConstraints(context, cs) // When selectedClockSize is the initial value, make both clocks invisible to avoid // flickering diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index ee4f41ddd5a0..6096cf74a772 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -186,12 +186,23 @@ constructor( constraints.apply { connect(customR.id.lockscreen_clock_view_large, START, PARENT_ID, START) connect(customR.id.lockscreen_clock_view_large, END, guideline, END) - connect(customR.id.lockscreen_clock_view_large, BOTTOM, R.id.device_entry_icon_view, TOP) + connect( + customR.id.lockscreen_clock_view_large, + BOTTOM, + R.id.device_entry_icon_view, + TOP, + ) val largeClockTopMargin = keyguardClockViewModel.getLargeClockTopMargin() + getDimen(DATE_WEATHER_VIEW_HEIGHT) + getDimen(ENHANCED_SMARTSPACE_HEIGHT) - connect(customR.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin) + connect( + customR.id.lockscreen_clock_view_large, + TOP, + PARENT_ID, + TOP, + largeClockTopMargin, + ) constrainWidth(customR.id.lockscreen_clock_view_large, WRAP_CONTENT) // The following two lines make lockscreen_clock_view_large is constrained to available diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index c11005d38986..a595d815e016 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -93,18 +93,18 @@ class ClockSizeTransition( fromBounds: Rect, toBounds: Rect, fromSSBounds: Rect?, - toSSBounds: Rect? + toSSBounds: Rect?, ) {} override fun createAnimator( sceenRoot: ViewGroup, startValues: TransitionValues?, - endValues: TransitionValues? + endValues: TransitionValues?, ): Animator? { if (startValues == null || endValues == null) { Log.w( TAG, - "Couldn't create animator: startValues=$startValues; endValues=$endValues" + "Couldn't create animator: startValues=$startValues; endValues=$endValues", ) return null } @@ -137,7 +137,7 @@ class ClockSizeTransition( "Skipping no-op transition: $toView; " + "vis: $fromVis -> $toVis; " + "alpha: $fromAlpha -> $toAlpha; " + - "bounds: $fromBounds -> $toBounds; " + "bounds: $fromBounds -> $toBounds; ", ) } return null @@ -151,7 +151,7 @@ class ClockSizeTransition( lerp(fromBounds.left, toBounds.left, fract), lerp(fromBounds.top, toBounds.top, fract), lerp(fromBounds.right, toBounds.right, fract), - lerp(fromBounds.bottom, toBounds.bottom, fract) + lerp(fromBounds.bottom, toBounds.bottom, fract), ) fun assignAnimValues(src: String, fract: Float, vis: Int? = null) { @@ -160,7 +160,7 @@ class ClockSizeTransition( if (DEBUG) { Log.i( TAG, - "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;" + "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;", ) } toView.setVisibility(vis ?: View.VISIBLE) @@ -174,7 +174,7 @@ class ClockSizeTransition( "transitioning: $toView; " + "vis: $fromVis -> $toVis; " + "alpha: $fromAlpha -> $toAlpha; " + - "bounds: $fromBounds -> $toBounds; " + "bounds: $fromBounds -> $toBounds; ", ) } @@ -258,7 +258,7 @@ class ClockSizeTransition( fromBounds: Rect, toBounds: Rect, fromSSBounds: Rect?, - toSSBounds: Rect? + toSSBounds: Rect?, ) { // Move normally if clock is not changing visibility if (fromIsVis == toIsVis) return @@ -347,12 +347,17 @@ class ClockSizeTransition( fromBounds: Rect, toBounds: Rect, fromSSBounds: Rect?, - toSSBounds: Rect? + toSSBounds: Rect?, ) { // If view is changing visibility, hold it in place if (fromIsVis == toIsVis) return if (DEBUG) Log.i(TAG, "Holding position of ${view.id}") - toBounds.set(fromBounds) + + if (fromIsVis) { + toBounds.set(fromBounds) + } else { + fromBounds.set(toBounds) + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index 5c79c0b5c1bb..82adced1e1be 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -181,7 +181,7 @@ constructor( fun getLargeClockTopMargin(): Int { return systemBarUtils.getStatusBarHeight() + resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) + - resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) } val largeClockTopMargin: Flow<Int> = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt index 6579ea162ee2..65c0f57b76f5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context import com.android.internal.policy.SystemBarUtils +import com.android.systemui.customization.R as customR import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.res.R @@ -39,20 +40,16 @@ constructor( val selectedClockSize: StateFlow<ClockSizeSetting> = interactor.selectedClockSize val shouldHideSmartspace: Flow<Boolean> = - combine( - interactor.selectedClockSize, - interactor.currentClockId, - ::Pair, - ) - .map { (size, currentClockId) -> - when (size) { - // TODO (b/284122375) This is temporary. We should use clockController - // .largeClock.config.hasCustomWeatherDataDisplay instead, but - // ClockRegistry.createCurrentClock is not reliable. - ClockSizeSetting.DYNAMIC -> currentClockId == "DIGITAL_CLOCK_WEATHER" - ClockSizeSetting.SMALL -> false - } + combine(interactor.selectedClockSize, interactor.currentClockId, ::Pair).map { + (size, currentClockId) -> + when (size) { + // TODO (b/284122375) This is temporary. We should use clockController + // .largeClock.config.hasCustomWeatherDataDisplay instead, but + // ClockRegistry.createCurrentClock is not reliable. + ClockSizeSetting.DYNAMIC -> currentClockId == "DIGITAL_CLOCK_WEATHER" + ClockSizeSetting.SMALL -> false } + } fun getSmartspaceStartPadding(context: Context): Int { return KeyguardSmartspaceViewModel.getSmartspaceStartMargin(context) @@ -83,7 +80,7 @@ constructor( } else { getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + SystemBarUtils.getStatusBarHeight(context) + - getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 850e943d17eb..ef6ae0dd6427 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -17,8 +17,10 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Resources +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.annotations.VisibleForTesting import com.android.systemui.biometrics.AuthController +import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor @@ -42,7 +44,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch class LockscreenContentViewModel @AssistedInject @@ -82,10 +83,7 @@ constructor( unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true), unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false), ) { start, end -> - UnfoldTranslations( - start = start, - end = end, - ) + UnfoldTranslations(start = start, end = end) } .collect { _unfoldTranslations.value = it } } @@ -102,17 +100,15 @@ constructor( /** Returns a flow that indicates whether lockscreen notifications should be rendered. */ fun areNotificationsVisible(): Flow<Boolean> { - return combine( - clockSize, - shadeInteractor.isShadeLayoutWide, - ) { clockSize, isShadeLayoutWide -> + return combine(clockSize, shadeInteractor.isShadeLayoutWide) { clockSize, isShadeLayoutWide + -> clockSize == ClockSize.SMALL || isShadeLayoutWide } } fun getSmartSpacePaddingTop(resources: Resources): Int { return if (clockSize.value == ClockSize.LARGE) { - resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + + resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) + resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) } else { 0 diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt index d38e507082b9..913aa6f9d547 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt @@ -42,7 +42,7 @@ import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.controls.util.SessionTokenFactory import com.android.systemui.res.R -import com.android.systemui.util.Assert +import com.android.systemui.util.concurrency.Execution import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -63,6 +63,7 @@ constructor( @Background private val looper: Looper, @Background private val handler: Handler, @Background private val bgScope: CoroutineScope, + private val execution: Execution, ) { /** @@ -108,7 +109,7 @@ constructor( m3controller: Media3Controller, token: SessionToken, ): MediaButton? { - Assert.isNotMainThread() + require(!execution.isMainThread()) // First, get standard actions val playOrPause = diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index f49693a8700b..80ac2fcd4aa4 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -696,7 +696,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack TaskStackChangeListeners.getInstance().unregisterTaskStackListener( mTaskStackListener); DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); - mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(null)); + mPipOptional.ifPresent(pip -> pip.removeOnIsInPipStateChangedListener( + mOnIsInPipStateChangedListener)); try { mWindowManagerService.unregisterSystemGestureExclusionListener( @@ -720,7 +721,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mTaskStackListener); DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, mUiThreadContext.getExecutor()::execute, mOnPropertiesChangedListener); - mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener( + mPipOptional.ifPresent(pip -> pip.addOnIsInPipStateChangedListener( mOnIsInPipStateChangedListener)); mDesktopModeOptional.ifPresent( dm -> dm.addDesktopGestureExclusionRegionListener( diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index 8d48c1d1d23f..1cf4c23415da 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -26,11 +26,13 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.PowerRepository import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessModel import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.statusbar.phone.ScreenOffAnimationController import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -56,7 +58,7 @@ constructor( * Unless you need to respond differently to different [WakeSleepReason]s, you should use * [isAwake]. */ - val detailedWakefulness = repository.wakefulness + val detailedWakefulness: StateFlow<WakefulnessModel> = repository.wakefulness /** * Whether we're awake (screen is on and responding to user touch) or asleep (screen is off, or @@ -189,9 +191,7 @@ constructor( * In tests, you should be able to use [setAsleepForTest] rather than calling this method * directly. */ - fun onFinishedGoingToSleep( - powerButtonLaunchGestureTriggeredDuringSleep: Boolean, - ) { + fun onFinishedGoingToSleep(powerButtonLaunchGestureTriggeredDuringSleep: Boolean) { // If the launch gesture was previously detected via onCameraLaunchGestureDetected, carry // that state forward. It will be reset by the next onStartedGoingToSleep. val powerButtonLaunchGestureTriggered = @@ -255,7 +255,7 @@ constructor( @JvmOverloads fun PowerInteractor.setAwakeForTest( @PowerManager.WakeReason reason: Int = PowerManager.WAKE_REASON_UNKNOWN, - forceEmit: Boolean = false + forceEmit: Boolean = false, ) { emitDuplicateWakefulnessValue = forceEmit @@ -286,9 +286,7 @@ constructor( emitDuplicateWakefulnessValue = forceEmit this.onStartedGoingToSleep(reason = sleepReason) - this.onFinishedGoingToSleep( - powerButtonLaunchGestureTriggeredDuringSleep = false, - ) + this.onFinishedGoingToSleep(powerButtonLaunchGestureTriggeredDuringSleep = false) } /** Helper method for tests to simulate the device screen state change event. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt new file mode 100644 index 000000000000..58834037e2b7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.data.repository + +import android.content.res.Resources +import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.util.kotlin.emitOnStart +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class LargeTileSpanRepository +@Inject +constructor( + @Application scope: CoroutineScope, + @Main private val resources: Resources, + configurationRepository: ConfigurationRepository, +) { + val span: StateFlow<Int> = + configurationRepository.onConfigurationChange + .emitOnStart() + .mapLatest { + if (resources.configuration.fontScale >= FONT_SCALE_THRESHOLD) { + resources.getInteger(R.integer.quick_settings_infinite_grid_tile_max_width) + } else { + 2 + } + } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.WhileSubscribed(), 2) + + private companion object { + const val FONT_SCALE_THRESHOLD = 2f + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt index 971598dea0bd..b0c607303297 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt @@ -17,9 +17,16 @@ package com.android.systemui.qs.panels.data.repository import android.content.Context +import android.content.IntentFilter import android.content.SharedPreferences +import com.android.systemui.backup.BackupHelper +import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.settings.UserFileManager import com.android.systemui.user.data.repository.UserRepository @@ -29,9 +36,11 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** Repository for QS user preferences. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -43,26 +52,31 @@ constructor( private val userRepository: UserRepository, private val defaultLargeTilesRepository: DefaultLargeTilesRepository, @Background private val backgroundDispatcher: CoroutineDispatcher, + @PanelsLog private val logBuffer: LogBuffer, + broadcastDispatcher: BroadcastDispatcher, ) { - /** Whether to show the labels on icon tiles for the current user. */ - val showLabels: Flow<Boolean> = - userRepository.selectedUserInfo - .flatMapLatest { userInfo -> - val prefs = getSharedPrefs(userInfo.id) - prefs.observe().emitOnStart().map { prefs.getBoolean(ICON_LABELS_KEY, false) } - } - .flowOn(backgroundDispatcher) + private val logger by lazy { Logger(logBuffer, TAG) } + + private val backupRestorationEvents: Flow<Unit> = + broadcastDispatcher + .broadcastFlow( + filter = IntentFilter(ACTION_RESTORE_FINISHED), + flags = Context.RECEIVER_NOT_EXPORTED, + permission = BackupHelper.PERMISSION_SELF, + ) + .onEach { logger.i("Restored state for QS preferences.") } + .emitOnStart() /** Set of [TileSpec] to display as large tiles for the current user. */ val largeTilesSpecs: Flow<Set<TileSpec>> = - userRepository.selectedUserInfo - .flatMapLatest { userInfo -> + combine(backupRestorationEvents, userRepository.selectedUserInfo, ::Pair) + .flatMapLatest { (_, userInfo) -> val prefs = getSharedPrefs(userInfo.id) prefs.observe().emitOnStart().map { prefs .getStringSet( LARGE_TILES_SPECS_KEY, - defaultLargeTilesRepository.defaultLargeTiles.map { it.spec }.toSet() + defaultLargeTilesRepository.defaultLargeTiles.map { it.spec }.toSet(), ) ?.map { TileSpec.create(it) } ?.toSet() ?: defaultLargeTilesRepository.defaultLargeTiles @@ -70,13 +84,6 @@ constructor( } .flowOn(backgroundDispatcher) - /** Sets for the current user whether to show the labels on icon tiles. */ - fun setShowLabels(showLabels: Boolean) { - with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) { - edit().putBoolean(ICON_LABELS_KEY, showLabels).apply() - } - } - /** Sets for the current user the set of [TileSpec] to display as large tiles. */ fun setLargeTilesSpecs(specs: Set<TileSpec>) { with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) { @@ -85,15 +92,11 @@ constructor( } private fun getSharedPrefs(userId: Int): SharedPreferences { - return userFileManager.getSharedPreferences( - FILE_NAME, - Context.MODE_PRIVATE, - userId, - ) + return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, userId) } companion object { - private const val ICON_LABELS_KEY = "show_icon_labels" + private const val TAG = "QSPreferencesRepository" private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" const val FILE_NAME = "quick_settings_prefs" } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/backup/QSPreferencesBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/backup/QSPreferencesBackupHelper.kt new file mode 100644 index 000000000000..2c51ca9c253f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/backup/QSPreferencesBackupHelper.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.domain.backup + +import android.app.backup.SharedPreferencesBackupHelper +import android.content.Context +import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository.Companion.FILE_NAME +import com.android.systemui.settings.UserFileManagerImpl + +class QSPreferencesBackupHelper(context: Context, userId: Int) : + SharedPreferencesBackupHelper( + context, + UserFileManagerImpl.createFile(userId = userId, fileName = FILE_NAME).path, + ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt index ec61a0d5769e..23c79f576df5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt @@ -21,12 +21,14 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository +import com.android.systemui.qs.panels.data.repository.LargeTileSpanRepository import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -38,6 +40,7 @@ constructor( private val repo: DefaultLargeTilesRepository, private val currentTilesInteractor: CurrentTilesInteractor, private val preferencesInteractor: QSPreferencesInteractor, + largeTilesSpanRepo: LargeTileSpanRepository, @PanelsLog private val logBuffer: LogBuffer, @Application private val applicationScope: CoroutineScope, ) { @@ -46,6 +49,8 @@ constructor( .onEach { logChange(it) } .stateIn(applicationScope, SharingStarted.Eagerly, repo.defaultLargeTiles) + val largeTilesSpan: StateFlow<Int> = largeTilesSpanRepo.span + fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec) fun setLargeTiles(specs: Set<TileSpec>) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt index 74fa0fef21d7..c729c7c15176 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt @@ -37,13 +37,17 @@ import com.android.systemui.qs.pipeline.shared.TileSpec fun rememberEditListState( tiles: List<SizedTile<EditTileViewModel>>, columns: Int, + largeTilesSpan: Int, ): EditTileListState { - return remember(tiles, columns) { EditTileListState(tiles, columns) } + return remember(tiles, columns) { EditTileListState(tiles, columns, largeTilesSpan) } } /** Holds the temporary state of the tile list during a drag movement where we move tiles around. */ -class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val columns: Int) : - DragAndDropState { +class EditTileListState( + tiles: List<SizedTile<EditTileViewModel>>, + private val columns: Int, + private val largeTilesSpan: Int, +) : DragAndDropState { private val _draggedCell = mutableStateOf<SizedTile<EditTileViewModel>?>(null) override val draggedCell get() = _draggedCell.value @@ -86,7 +90,7 @@ class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val c if (fromIndex != -1) { val cell = _tiles.removeAt(fromIndex) cell as TileGridCell - _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) 2 else 1)) + _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) largeTilesSpan else 1)) regenerateGrid(fromIndex) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index d10722287f5d..4a51bf06d4af 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -33,7 +33,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -63,6 +62,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.modifiers.size import com.android.compose.modifiers.thenIf +import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon @@ -194,26 +194,35 @@ fun SmallTileContent( is Icon.Resource -> context.getDrawable(icon.res) } } - if (loadedDrawable !is Animatable) { - Icon(icon = icon, tint = animatedColor, modifier = iconModifier) - } else if (icon is Icon.Resource) { - val image = AnimatedImageVector.animatedVectorResource(id = icon.res) + if (loadedDrawable is Animatable) { val painter = - key(icon) { - if (animateToEnd) { - rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true) - } else { - var atEnd by remember(icon.res) { mutableStateOf(false) } - LaunchedEffect(key1 = icon.res) { atEnd = true } - rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd) + when (icon) { + is Icon.Resource -> { + val image = AnimatedImageVector.animatedVectorResource(id = icon.res) + key(icon) { + if (animateToEnd) { + rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true) + } else { + var atEnd by remember(icon.res) { mutableStateOf(false) } + LaunchedEffect(key1 = icon.res) { atEnd = true } + rememberAnimatedVectorPainter( + animatedImageVector = image, + atEnd = atEnd, + ) + } + } } + is Icon.Loaded -> rememberDrawablePainter(loadedDrawable) } + Image( painter = painter, contentDescription = icon.contentDescription?.load(), colorFilter = ColorFilter.tint(color = animatedColor), modifier = iconModifier, ) + } else { + Icon(icon = icon, tint = animatedColor, modifier = iconModifier) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index b5cec120987f..31ea60e2f0bc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -26,7 +26,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.Orientation @@ -173,6 +173,7 @@ fun DefaultEditTileGrid( listState: EditTileListState, otherTiles: List<SizedTile<EditTileViewModel>>, columns: Int, + largeTilesSpan: Int, modifier: Modifier, onRemoveTile: (TileSpec) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, @@ -203,7 +204,7 @@ fun DefaultEditTileGrid( containerColor = Color.Transparent, topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) }, ) { innerPadding -> - CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + CompositionLocalProvider(LocalOverscrollFactory provides null) { val scrollState = rememberScrollState() LaunchedEffect(listState.dragInProgress) { if (listState.dragInProgress) { @@ -230,7 +231,14 @@ fun DefaultEditTileGrid( } } - CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles) + CurrentTilesGrid( + listState, + selectionState, + columns, + largeTilesSpan, + onResize, + onSetTiles, + ) // Hide available tiles when dragging AnimatedVisibility( @@ -273,7 +281,7 @@ private fun EditGridHeader( ) { Box( contentAlignment = Alignment.Center, - modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight), + modifier = modifier.fillMaxWidth().wrapContentHeight(), ) { content() } @@ -300,6 +308,7 @@ private fun CurrentTilesGrid( listState: EditTileListState, selectionState: MutableSelectionState, columns: Int, + largeTilesSpan: Int, onResize: (TileSpec, toIcon: Boolean) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, ) { @@ -340,7 +349,8 @@ private fun CurrentTilesGrid( } .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { - EditTiles(cells, columns, listState, selectionState, coroutineScope) { spec -> + EditTiles(cells, columns, listState, selectionState, coroutineScope, largeTilesSpan) { spec + -> // Toggle the current size of the tile currentListState.isIcon(spec)?.let { onResize(spec, !it) } } @@ -425,6 +435,7 @@ fun LazyGridScope.EditTiles( dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, coroutineScope: CoroutineScope, + largeTilesSpan: Int, onToggleSize: (spec: TileSpec) -> Unit, ) { items( @@ -456,6 +467,7 @@ fun LazyGridScope.EditTiles( onToggleSize = onToggleSize, coroutineScope = coroutineScope, bounceableInfo = cells.bounceableInfo(index, columns), + largeTilesSpan = largeTilesSpan, modifier = Modifier.animateItem(), ) } @@ -472,6 +484,7 @@ private fun TileGridCell( selectionState: MutableSelectionState, onToggleSize: (spec: TileSpec) -> Unit, coroutineScope: CoroutineScope, + largeTilesSpan: Int, bounceableInfo: BounceableInfo, modifier: Modifier = Modifier, ) { @@ -514,8 +527,11 @@ private fun TileGridCell( .fillMaxWidth() .onSizeChanged { // Grab the size before the bounceable to get the idle width - val min = if (cell.isIcon) it.width else (it.width - padding) / 2 - val max = if (cell.isIcon) (it.width * 2) + padding else it.width + val totalPadding = (largeTilesSpan - 1) * padding + val min = + if (cell.isIcon) it.width else (it.width - totalPadding) / largeTilesSpan + val max = + if (cell.isIcon) (it.width * largeTilesSpan) + totalPadding else it.width tileWidths = TileWidths(it.width, min, max) } .bounceable( @@ -554,15 +570,13 @@ private fun TileGridCell( val targetValue = if (cell.isIcon) 0f else 1f val animatedProgress = remember { Animatable(targetValue) } - if (selected) { - val resizingState = selectionState.resizingState - LaunchedEffect(targetValue, resizingState) { - if (resizingState == null) { - animatedProgress.animateTo(targetValue) - } else { - snapshotFlow { resizingState.progression } - .collectLatest { animatedProgress.snapTo(it) } - } + val resizingState = selectionState.resizingState?.takeIf { selected } + LaunchedEffect(targetValue, resizingState) { + if (resizingState == null) { + animatedProgress.animateTo(targetValue) + } else { + snapshotFlow { resizingState.progression } + .collectLatest { animatedProgress.snapTo(it) } } } @@ -705,7 +719,6 @@ private fun Modifier.tileBackground(color: Color): Modifier { private object EditModeTileDefaults { const val PLACEHOLDER_ALPHA = .3f - val EditGridHeaderHeight = 60.dp val CurrentTilesGridPadding = 8.dp @Composable diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 5ac2ad02d671..29ff1715dea2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -79,7 +79,8 @@ constructor( } val columns = columnsWithMediaViewModel.columns - val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) } + val largeTilesSpan by iconTilesViewModel.largeTilesSpanState + val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width(largeTilesSpan)) } val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() @@ -129,21 +130,23 @@ constructor( viewModel.columnsWithMediaViewModelFactory.createWithoutMediaTracking() } val columns = columnsViewModel.columns + val largeTilesSpan by iconTilesViewModel.largeTilesSpanState val largeTiles by iconTilesViewModel.largeTiles.collectAsStateWithLifecycle() // Non-current tiles should always be displayed as icon tiles. val sizedTiles = - remember(tiles, largeTiles) { + remember(tiles, largeTiles, largeTilesSpan) { tiles.map { SizedTileImpl( it, - if (!it.isCurrent || !largeTiles.contains(it.tileSpec)) 1 else 2, + if (!it.isCurrent || !largeTiles.contains(it.tileSpec)) 1 + else largeTilesSpan, ) } } val (currentTiles, otherTiles) = sizedTiles.partition { it.tile.isCurrent } - val currentListState = rememberEditListState(currentTiles, columns) + val currentListState = rememberEditListState(currentTiles, columns, largeTilesSpan) DefaultEditTileGrid( listState = currentListState, otherTiles = otherTiles, @@ -154,6 +157,7 @@ constructor( onResize = iconTilesViewModel::resize, onStopEditing = onStopEditing, onReset = viewModel::showResetDialog, + largeTilesSpan = largeTilesSpan, ) } @@ -171,7 +175,7 @@ constructor( .map { it.flatten().map { it.tile } } } - private fun TileSpec.width(): Int { - return if (iconTilesViewModel.isIconTile(this)) 1 else 2 + private fun TileSpec.width(largeSize: Int = iconTilesViewModel.largeTilesSpan.value): Int { + return if (iconTilesViewModel.isIconTile(this)) 1 else largeSize } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 5bebdbc7a13e..9bbf290a53f0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -156,6 +156,14 @@ fun Tile( bounceEnd = currentBounceableInfo.bounceEnd, ), ) { expandable -> + val longClick: (() -> Unit)? = + { + hapticsViewModel?.setTileInteractionState( + TileHapticsViewModel.TileInteractionState.LONG_CLICKED + ) + tile.onLongClick(expandable) + } + .takeIf { uiState.handlesLongClick } TileContainer( onClick = { tile.onClick(expandable) @@ -166,12 +174,7 @@ fun Tile( coroutineScope.launch { currentBounceableInfo.bounceable.animateBounce() } } }, - onLongClick = { - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.LONG_CLICKED - ) - tile.onLongClick(expandable) - }, + onLongClick = longClick, uiState = uiState, iconOnly = iconOnly, ) { @@ -192,14 +195,6 @@ fun Tile( tile.onSecondaryClick() } .takeIf { uiState.handlesSecondaryClick } - val longClick: (() -> Unit)? = - { - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.LONG_CLICKED - ) - tile.onLongClick(expandable) - } - .takeIf { uiState.handlesLongClick } LargeTileContent( label = uiState.label, secondaryLabel = uiState.secondaryLabel, @@ -237,7 +232,7 @@ private fun TileExpandable( @Composable fun TileContainer( onClick: () -> Unit, - onLongClick: () -> Unit, + onLongClick: (() -> Unit)?, uiState: TileUiState, iconOnly: Boolean, content: @Composable BoxScope.() -> Unit, @@ -281,7 +276,7 @@ fun Modifier.tilePadding(): Modifier { @Composable fun Modifier.tileCombinedClickable( onClick: () -> Unit, - onLongClick: () -> Unit, + onLongClick: (() -> Unit)?, uiState: TileUiState, iconOnly: Boolean, ): Modifier { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt index 9552aa935bbf..41c3de55af70 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue import com.android.systemui.qs.panels.ui.compose.selection.ResizingDefaults.RESIZING_THRESHOLD -class ResizingState(private val widths: TileWidths, private val onResize: () -> Unit) { +class ResizingState(val widths: TileWidths, private val onResize: () -> Unit) { /** Total drag offset of this resize operation. */ private var totalOffset by mutableFloatStateOf(0f) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt index 9feaab83cc1f..a9d673aa7400 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt @@ -17,9 +17,13 @@ package com.android.systemui.qs.panels.ui.viewmodel import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.panels.domain.interactor.DynamicIconTilesInteractor import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch /** View model to resize QS tiles down to icons when removed from the current tiles. */ class DynamicIconTilesViewModel @@ -28,10 +32,21 @@ constructor( interactorFactory: DynamicIconTilesInteractor.Factory, iconTilesViewModel: IconTilesViewModel, ) : IconTilesViewModel by iconTilesViewModel, ExclusiveActivatable() { + private val hydrator = Hydrator("DynamicIconTilesViewModel") private val interactor = interactorFactory.create() + val largeTilesSpanState = + hydrator.hydratedStateOf( + traceName = "largeTilesSpan", + source = iconTilesViewModel.largeTilesSpan, + ) + override suspend fun onActivated(): Nothing { - interactor.activate() + coroutineScope { + launch { hydrator.activate() } + launch { interactor.activate() } + awaitCancellation() + } } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt index 4e698edf4e34..b8c5fbb72614 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt @@ -25,6 +25,8 @@ import kotlinx.coroutines.flow.StateFlow interface IconTilesViewModel { val largeTiles: StateFlow<Set<TileSpec>> + val largeTilesSpan: StateFlow<Int> + fun isIconTile(spec: TileSpec): Boolean fun resize(spec: TileSpec, toIcon: Boolean) @@ -34,6 +36,7 @@ interface IconTilesViewModel { class IconTilesViewModelImpl @Inject constructor(private val interactor: IconTilesInteractor) : IconTilesViewModel { override val largeTiles = interactor.largeTilesSpecs + override val largeTilesSpan = interactor.largeTilesSpan override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt index 33ce5519b68c..adc4e4bf0870 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt @@ -70,18 +70,21 @@ constructor( source = quickQuickSettingsRowInteractor.rows, ) + private val largeTilesSpan by + hydrator.hydratedStateOf( + traceName = "largeTilesSpan", + source = iconTilesViewModel.largeTilesSpan, + ) + private val currentTiles by hydrator.hydratedStateOf(traceName = "currentTiles", source = tilesInteractor.currentTiles) val tileViewModels by derivedStateOf { currentTiles - .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } + .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width()) } .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } } - private val TileSpec.width: Int - get() = if (largeTiles.contains(this)) 2 else 1 - override suspend fun onActivated(): Nothing { coroutineScope { launch { hydrator.activate() } @@ -95,4 +98,6 @@ constructor( interface Factory { fun create(): QuickQuickSettingsViewModel } + + private fun TileSpec.width(): Int = if (largeTiles.contains(this)) largeTilesSpan else 1 } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java index f218d86a5aa1..37d24debe958 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java @@ -243,15 +243,15 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> { } mSelectedCard = cards.get(selectedIndex); switch (mSelectedCard.getCardImage().getType()) { + case TYPE_BITMAP: + case TYPE_ADAPTIVE_BITMAP: + mCardViewDrawable = mSelectedCard.getCardImage().loadDrawable(mContext); + break; case TYPE_URI: case TYPE_URI_ADAPTIVE_BITMAP: - mCardViewDrawable = null; - break; case TYPE_RESOURCE: - case TYPE_BITMAP: - case TYPE_ADAPTIVE_BITMAP: case TYPE_DATA: - mCardViewDrawable = mSelectedCard.getCardImage().loadDrawable(mContext); + mCardViewDrawable = null; break; default: Log.e(TAG, "Unknown icon type: " + mSelectedCard.getCardImage().getType()); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index 42d4effbac3a..63510b873951 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.res.Resources import android.view.LayoutInflater import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY +import com.android.systemui.CoreStartable import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.ConfigurationStateImpl import com.android.systemui.common.ui.GlobalConfig @@ -29,12 +30,16 @@ import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R +import com.android.systemui.shade.data.repository.ShadePositionRepository +import com.android.systemui.shade.data.repository.ShadePositionRepositoryImpl import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.phone.ConfigurationControllerImpl import com.android.systemui.statusbar.phone.ConfigurationForwarder import com.android.systemui.statusbar.policy.ConfigurationController import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap /** * Module responsible for managing display-specific components and resources for the notification @@ -149,4 +154,25 @@ object ShadeDisplayAwareModule { configurationInteractor } } + + @SysUISingleton + @Provides + @ShadeDisplayAware + fun provideShadePositionRepository(impl: ShadePositionRepositoryImpl): ShadePositionRepository { + ShadeWindowGoesAround.isUnexpectedlyInLegacyMode() + return impl + } + + @Provides + @IntoMap + @ClassKey(ShadePositionRepositoryImpl::class) + fun provideShadePositionRepositoryAsCoreStartable( + impl: ShadePositionRepositoryImpl + ): CoreStartable { + return if (ShadeWindowGoesAround.isEnabled) { + impl + } else { + CoreStartable.NOP + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt new file mode 100644 index 000000000000..802fc0edb533 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import android.view.Display +import com.android.systemui.shade.data.repository.ShadePositionRepository +import com.android.systemui.statusbar.commandline.Command +import java.io.PrintWriter + +class ShadePrimaryDisplayCommand(private val positionRepository: ShadePositionRepository) : + Command { + + override fun execute(pw: PrintWriter, args: List<String>) { + if (args[0].lowercase() == "reset") { + positionRepository.resetDisplayId() + pw.println("Reset shade primary display id to ${Display.DEFAULT_DISPLAY}") + return + } + + val displayId: Int = + try { + args[0].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return + } + + if (displayId < 0) { + pw.println("Error: display id should be positive integer") + } + + positionRepository.setDisplayId(displayId) + pw.println("New shade primary display id is $displayId") + } + + override fun help(pw: PrintWriter) { + pw.println("shade_display_override <displayId> ") + pw.println("Set the display which is holding the shade.") + pw.println("shade_display_override reset ") + pw.println("Reset the display which is holding the shade.") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt new file mode 100644 index 000000000000..24c067ae2371 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.data.repository + +import android.view.Display +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.ShadePrimaryDisplayCommand +import com.android.systemui.statusbar.commandline.CommandRegistry +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +interface ShadePositionRepository { + /** ID of the display which currently hosts the shade */ + val displayId: StateFlow<Int> + + /** + * Updates the value of the shade display id stored, emitting to the new display id to every + * component dependent on the shade display id + */ + fun setDisplayId(displayId: Int) + + /** Resets value of shade primary display to the default display */ + fun resetDisplayId() +} + +/** Source of truth for the display currently holding the shade. */ +@SysUISingleton +class ShadePositionRepositoryImpl +@Inject +constructor(private val commandRegistry: CommandRegistry) : ShadePositionRepository, CoreStartable { + private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY) + + override val displayId: StateFlow<Int> + get() = _displayId + + override fun setDisplayId(displayId: Int) { + _displayId.value = displayId + } + + override fun resetDisplayId() { + _displayId.value = Display.DEFAULT_DISPLAY + } + + override fun start() { + commandRegistry.registerCommand("shade_display_override") { + ShadePrimaryDisplayCommand(this) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java index 3a31851bcb4f..52a79d39bb3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.connectivity; +import static com.android.systemui.Flags.multiuserWifiPickerTrackerSupport; + +import android.content.Context; import android.content.Intent; import android.os.UserHandle; import android.os.UserManager; @@ -65,13 +68,16 @@ public class AccessPointControllerImpl implements AccessPointController, private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); private int mCurrentUser; + private Context mContext; public AccessPointControllerImpl( + Context context, UserManager userManager, UserTracker userTracker, Executor mainExecutor, WifiPickerTrackerFactory wifiPickerTrackerFactory ) { + mContext = context; mUserManager = userManager; mUserTracker = userTracker; mCurrentUser = userTracker.getUserId(); @@ -87,7 +93,11 @@ public class AccessPointControllerImpl implements AccessPointController, */ public void init() { if (mWifiPickerTracker == null) { - mWifiPickerTracker = mWifiPickerTrackerFactory.create(this.getLifecycle(), this, TAG); + // We are creating the WifiPickerTracker during init to make sure we have one + // available at all times however we expect this to be recreated very quickly + // with a user-specific context in onUserSwitched. + mWifiPickerTracker = + mWifiPickerTrackerFactory.create(mContext, this.getLifecycle(), this, TAG); } } @@ -116,6 +126,19 @@ public class AccessPointControllerImpl implements AccessPointController, void onUserSwitched(int newUserId) { mCurrentUser = newUserId; + // Return early if multiuser support is not enabled. + if (!multiuserWifiPickerTrackerSupport()) { + return; + } + + if (mWifiPickerTracker != null) { + mMainExecutor.execute(() -> mLifecycle.setCurrentState(Lifecycle.State.CREATED)); + } + Context context = mContext.createContextAsUser(UserHandle.of(newUserId), /* flags= */ 0); + mWifiPickerTracker = mWifiPickerTrackerFactory.create(context, mLifecycle, this, TAG); + if (!mCallbacks.isEmpty()) { + mMainExecutor.execute(() -> mLifecycle.setCurrentState(Lifecycle.State.STARTED)); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt index dc2ebe55fb73..947ce33274d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt @@ -22,6 +22,7 @@ import android.net.wifi.WifiManager import android.os.Handler import android.os.SimpleClock import androidx.lifecycle.Lifecycle +import com.android.systemui.Flags.multiuserWifiPickerTrackerSupport import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.util.concurrency.ThreadFactory @@ -41,7 +42,7 @@ import javax.inject.Inject class WifiPickerTrackerFactory @Inject constructor( - private val context: Context, + private val applicationContext: Context, private val wifiManager: WifiManager?, private val connectivityManager: ConnectivityManager, private val systemClock: SystemClock, @@ -64,16 +65,23 @@ constructor( * @return a new [WifiPickerTracker] or null if [WifiManager] is null. */ fun create( + userContext: Context, lifecycle: Lifecycle, listener: WifiPickerTrackerCallback, name: String, ): WifiPickerTracker? { return if (wifiManager == null) { null - } else + } else { + val contextToUse = + if (multiuserWifiPickerTrackerSupport()) { + userContext + } else { + applicationContext + } WifiPickerTracker( lifecycle, - context, + contextToUse, wifiManager, connectivityManager, mainHandler, @@ -86,6 +94,7 @@ constructor( SCAN_INTERVAL_MILLIS, listener, ) + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt index f441fd644c17..4c54fc49e536 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt @@ -112,12 +112,12 @@ constructor( } override fun initializeStatusBar() { - StatusBarSimpleFragment.assertInLegacyMode() + StatusBarRootModernization.assertInLegacyMode() doStart() } private fun doStart() { - if (StatusBarSimpleFragment.isEnabled) doComposeStart() else doLegacyStart() + if (StatusBarRootModernization.isEnabled) doComposeStart() else doLegacyStart() } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt index f33b76b17f96..ff4760fd2837 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.core import android.view.Display import android.view.View +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.demomode.DemoModeController import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.DarkIconDispatcher @@ -46,12 +48,12 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.io.PrintWriter import java.util.Optional +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filterNotNull -import com.android.app.tracing.coroutines.launchTraced as launch /** * Class responsible for managing the lifecycle and state of the status bar. @@ -68,6 +70,7 @@ constructor( @Assisted private val statusBarModeRepository: StatusBarModePerDisplayRepository, @Assisted private val statusBarInitializer: StatusBarInitializer, @Assisted private val statusBarWindowController: StatusBarWindowController, + @Main private val mainContext: CoroutineContext, private val demoModeController: DemoModeController, private val pluginDependencyProvider: PluginDependencyProvider, private val autoHideController: AutoHideController, @@ -141,7 +144,8 @@ constructor( override fun start() { StatusBarConnectedDisplays.assertInNewMode() coroutineScope - .launch { + // Perform animations on the main thread to prevent crashes. + .launch(context = mainContext) { dumpManager.registerCriticalDumpable(dumpableName, this@StatusBarOrchestrator) launch { controllerAndBouncerShowing.collect { (controller, bouncerShowing) -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarSimpleFragment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt index 214151383dc6..057213fa4b18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarSimpleFragment.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt @@ -21,9 +21,9 @@ import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading and using the status bar simple fragment flag state */ -object StatusBarSimpleFragment { +object StatusBarRootModernization { /** Aconfig flag for removing the fragment */ - const val FLAG_NAME = Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_ROOT_MODERNIZATION /** A token used for dependency declaration */ val token: FlagToken @@ -32,7 +32,7 @@ object StatusBarSimpleFragment { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.statusBarSimpleFragment() + get() = Flags.statusBarRootModernization() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt index 958001625a07..1f8d365cfdad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt @@ -102,6 +102,10 @@ class NotifCollectionCache<V>( return --lives <= 0 } } + + override fun toString(): String { + return "$key = $value" + } } /** @@ -174,7 +178,10 @@ class NotifCollectionCache<V>( pw.println("$TAG(retainCount = $retainCount, purgeTimeoutMillis = $purgeTimeoutMillis)") pw.withIncreasedIndent { - pw.printCollection("keys present in cache", cache.keys.stream().sorted().toList()) + pw.printCollection( + "entries present in cache", + cache.values.stream().map { it.toString() }.sorted().toList(), + ) val misses = misses.get() val hits = hits.get() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt index 2fded34a56a0..e2328497d9ea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.annotation.SuppressLint import android.app.NotificationManager import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager @@ -50,7 +51,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import com.android.app.tracing.coroutines.launchTraced as launch /** * If the setting is enabled, this will track seen notifications and ensure that they only show in @@ -74,7 +74,7 @@ constructor( private val unseenNotifications = mutableSetOf<NotificationEntry>() private var isShadeVisible = false - private var unseenFilterEnabled = false + private var minimalismEnabled = false override fun attach(pipeline: NotifPipeline) { if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) { @@ -83,7 +83,7 @@ constructor( pipeline.addPromoter(unseenNotifPromoter) pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs) pipeline.addCollectionListener(collectionListener) - scope.launch { trackUnseenFilterSettingChanges() } + scope.launch { trackLockScreenNotificationMinimalismSettingChanges() } dumpManager.registerDumpable(this) } @@ -136,12 +136,12 @@ constructor( return seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled() } - private suspend fun trackUnseenFilterSettingChanges() { + private suspend fun trackLockScreenNotificationMinimalismSettingChanges() { // Only filter the seen notifs when the lock screen minimalism feature settings is on. minimalismFeatureSettingEnabled().collectLatest { isMinimalismSettingEnabled -> // update local field and invalidate if necessary - if (isMinimalismSettingEnabled != unseenFilterEnabled) { - unseenFilterEnabled = isMinimalismSettingEnabled + if (isMinimalismSettingEnabled != minimalismEnabled) { + minimalismEnabled = isMinimalismSettingEnabled unseenNotifications.clear() unseenNotifPromoter.invalidateList("unseen setting changed") } @@ -156,21 +156,21 @@ constructor( private val collectionListener = object : NotifCollectionListener { override fun onEntryAdded(entry: NotificationEntry) { - if (unseenFilterEnabled && !isShadeVisible) { + if (minimalismEnabled && !isShadeVisible) { logger.logUnseenAdded(entry.key) unseenNotifications.add(entry) } } override fun onEntryUpdated(entry: NotificationEntry) { - if (unseenFilterEnabled && !isShadeVisible) { + if (minimalismEnabled && !isShadeVisible) { logger.logUnseenUpdated(entry.key) unseenNotifications.add(entry) } } override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (unseenFilterEnabled && unseenNotifications.remove(entry)) { + if (minimalismEnabled && unseenNotifications.remove(entry)) { logger.logUnseenRemoved(entry.key) } } @@ -178,7 +178,7 @@ constructor( private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return - if (!unseenFilterEnabled) return + if (!minimalismEnabled) return // Only ever elevate a top unseen notification on keyguard, not even locked shade if (statusBarStateController.state != StatusBarState.KEYGUARD) { seenNotificationsInteractor.setTopOngoingNotification(null) @@ -215,6 +215,7 @@ constructor( override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = when { NotificationMinimalism.isUnexpectedlyInLegacyMode() -> false + !minimalismEnabled -> false seenNotificationsInteractor.isTopOngoingNotification(child) -> true !NotificationMinimalism.ungroupTopUnseen -> false else -> seenNotificationsInteractor.isTopUnseenNotification(child) @@ -225,6 +226,7 @@ constructor( object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { override fun isInSection(entry: ListEntry): Boolean { if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false + if (!minimalismEnabled) return false return entry.anyEntry { notificationEntry -> seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) } @@ -235,6 +237,7 @@ constructor( object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { override fun isInSection(entry: ListEntry): Boolean { if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false + if (!minimalismEnabled) return false return entry.anyEntry { notificationEntry -> seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 3bc549543ef2..5dff8120f33f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -21,12 +21,14 @@ import android.util.Log import android.view.View.GONE import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.res.R import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.shared.NotificationMinimalism @@ -38,6 +40,9 @@ import javax.inject.Inject import kotlin.math.max import kotlin.math.min import kotlin.properties.Delegates.notNull +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch private const val TAG = "NotifStackSizeCalc" private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG) @@ -56,7 +61,9 @@ constructor( private val lockscreenShadeTransitionController: LockscreenShadeTransitionController, private val mediaDataManager: MediaDataManager, @Main private val resources: Resources, - private val splitShadeStateController: SplitShadeStateController + private val splitShadeStateController: SplitShadeStateController, + private val seenNotificationsInteractor: SeenNotificationsInteractor, + @Application private val scope: CoroutineScope, ) { /** @@ -74,7 +81,7 @@ constructor( /** Whether we allow keyguard to show less important notifications above the shelf. */ private val limitLockScreenToOneImportant - get() = NotificationMinimalism.isEnabled + get() = NotificationMinimalism.isEnabled && minimalismSettingEnabled /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */ private var dividerHeight by notNull<Float>() @@ -85,8 +92,14 @@ constructor( */ private var saveSpaceOnLockscreen = false + /** True when the lock screen notification minimalism feature setting is enabled */ + private var minimalismSettingEnabled = false + init { updateResources() + if (NotificationMinimalism.isEnabled) { + scope.launch { trackLockScreenNotificationMinimalismSettingChanges() } + } } private fun allowedByPolicy(stackHeight: StackHeight): Boolean = @@ -199,7 +212,7 @@ constructor( canStackFitInSpace( heightResult, notifSpace = notifSpace, - shelfSpace = shelfSpace + shelfSpace = shelfSpace, ) == FitResult.FIT } @@ -229,7 +242,7 @@ constructor( canStackFitInSpace( heightResult, notifSpace = notifSpace, - shelfSpace = shelfSpace + shelfSpace = shelfSpace, ) != FitResult.NO_FIT } log { "\t--- maxNotifications=$maxNotifications" } @@ -277,7 +290,7 @@ constructor( fun computeHeight( stack: NotificationStackScrollLayout, maxNotifs: Int, - shelfHeight: Float + shelfHeight: Float, ): Float { log { "\n" } log { "computeHeight ---" } @@ -311,7 +324,7 @@ constructor( private enum class FitResult { FIT, FIT_IF_SAVE_SPACE, - NO_FIT + NO_FIT, } data class SpaceNeeded( @@ -319,7 +332,7 @@ constructor( val whenEnoughSpace: Float, // Float height of space needed when showing collapsed layout for FSI HUNs. - val whenSavingSpace: Float + val whenSavingSpace: Float, ) private data class StackHeight( @@ -335,9 +348,19 @@ constructor( val shelfHeightWithSpaceBefore: Float, /** Whether the stack should actually be forced into the shelf before this height. */ - val shouldForceIntoShelf: Boolean + val shouldForceIntoShelf: Boolean, ) + private suspend fun trackLockScreenNotificationMinimalismSettingChanges() { + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return + seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled().collectLatest { + if (it != minimalismSettingEnabled) { + minimalismSettingEnabled = it + } + Log.i(TAG, "minimalismSettingEnabled: $minimalismSettingEnabled") + } + } + private fun computeHeightPerNotificationLimit( stack: NotificationStackScrollLayout, shelfHeight: Float, @@ -377,7 +400,7 @@ constructor( stack, previous = currentNotification, current = children[firstViewInShelfIndex], - currentIndex = firstViewInShelfIndex + currentIndex = firstViewInShelfIndex, ) spaceBeforeShelf + shelfHeight } @@ -390,14 +413,15 @@ constructor( log { "\tcomputeHeightPerNotificationLimit i=$i notifs=$notifications " + "notifsHeightSavingSpace=$notifsWithCollapsedHun" + - " shelfWithSpaceBefore=$shelfWithSpaceBefore" + " shelfWithSpaceBefore=$shelfWithSpaceBefore" + + " limitLockScreenToOneImportant: $limitLockScreenToOneImportant" } yield( StackHeight( notifsHeight = notifications, notifsHeightSavingSpace = notifsWithCollapsedHun, shelfHeightWithSpaceBefore = shelfWithSpaceBefore, - shouldForceIntoShelf = counter?.shouldForceIntoShelf() ?: false + shouldForceIntoShelf = counter?.shouldForceIntoShelf() ?: false, ) ) } @@ -462,6 +486,10 @@ constructor( fun dump(pw: PrintWriter, args: Array<out String>) { pw.println("NotificationStackSizeCalculator saveSpaceOnLockscreen=$saveSpaceOnLockscreen") + pw.println( + "NotificationStackSizeCalculator " + + "limitLockScreenToOneImportant=$limitLockScreenToOneImportant" + ) } private fun ExpandableView.isShowable(onLockscreen: Boolean): Boolean { @@ -484,7 +512,7 @@ constructor( stack: NotificationStackScrollLayout, previous: ExpandableView?, current: ExpandableView?, - currentIndex: Int + currentIndex: Int, ): Float { if (currentIndex == 0) { return 0f @@ -536,11 +564,7 @@ constructor( takeWhile(predicate).count() - 1 /** Counts the number of notifications for each type of bucket */ - data class BucketTypeCounter( - var ongoing: Int = 0, - var important: Int = 0, - var other: Int = 0, - ) { + data class BucketTypeCounter(var ongoing: Int = 0, var important: Int = 0, var other: Int = 0) { fun incrementForBucket(@PriorityBucket bucket: Int?) { when (bucket) { BUCKET_MEDIA_CONTROLS, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 7389086296a3..80c8e8b2a109 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -203,7 +203,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.core.StatusBarConnectedDisplays; import com.android.systemui.statusbar.core.StatusBarInitializer; -import com.android.systemui.statusbar.core.StatusBarSimpleFragment; +import com.android.systemui.statusbar.core.StatusBarRootModernization; import com.android.systemui.statusbar.data.model.StatusBarMode; import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore; import com.android.systemui.statusbar.notification.NotificationActivityStarter; @@ -1231,7 +1231,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { checkBarModes(); }); } - if (!StatusBarSimpleFragment.isEnabled() && !StatusBarConnectedDisplays.isEnabled()) { + if (!StatusBarRootModernization.isEnabled() && !StatusBarConnectedDisplays.isEnabled()) { // When the flag is on, we register the fragment as a core startable and this is not // needed mStatusBarInitializer.initializeStatusBar(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt index ba878edc1132..58386b0cad7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt @@ -30,7 +30,7 @@ import com.android.systemui.statusbar.core.StatusBarInitializer import com.android.systemui.statusbar.core.StatusBarInitializerImpl import com.android.systemui.statusbar.core.StatusBarInitializerStore import com.android.systemui.statusbar.core.StatusBarOrchestrator -import com.android.systemui.statusbar.core.StatusBarSimpleFragment +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.data.repository.PrivacyDotViewControllerStoreModule import com.android.systemui.statusbar.data.repository.PrivacyDotWindowControllerStoreModule import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore @@ -87,7 +87,7 @@ interface StatusBarPhoneModule { return if (StatusBarConnectedDisplays.isEnabled) { // Will be started through MultiDisplayStatusBarStarter CoreStartable.NOP - } else if (StatusBarSimpleFragment.isEnabled) { + } else if (StatusBarRootModernization.isEnabled) { defaultInitializerLazy.get() } else { // Will be started through CentralSurfaces diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index 5cc44762dde8..c55a63c9edf5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -58,7 +58,7 @@ import com.android.systemui.statusbar.OperatorNameViewController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.core.StatusBarConnectedDisplays; -import com.android.systemui.statusbar.core.StatusBarSimpleFragment; +import com.android.systemui.statusbar.core.StatusBarRootModernization; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; @@ -365,7 +365,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mPrimaryOngoingActivityChip = mStatusBar.findViewById(R.id.ongoing_activity_chip_primary); mSecondaryOngoingActivityChip = mStatusBar.findViewById(R.id.ongoing_activity_chip_secondary); - if (!StatusBarSimpleFragment.isEnabled()) { + if (!StatusBarRootModernization.isEnabled()) { showEndSideContent(false); showClock(false); } @@ -464,7 +464,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue super.onPause(); mCommandQueue.removeCallback(this); mStatusBarStateController.removeCallback(this); - if (!StatusBarSimpleFragment.isEnabled()) { + if (!StatusBarRootModernization.isEnabled()) { mOngoingCallController.removeCallback(mOngoingCallListener); } mAnimationScheduler.removeCallback(this); @@ -507,7 +507,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mNicBindingDisposable = mNicViewBinder.bindWhileAttached(notificationIcons, displayId); } - if (!StatusBarSimpleFragment.isEnabled()) { + if (!StatusBarRootModernization.isEnabled()) { updateNotificationIconAreaAndOngoingActivityChip(/* animate= */ false); } Trace.endSection(); @@ -528,7 +528,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue new StatusBarVisibilityChangeListener() { @Override public void onStatusBarVisibilityMaybeChanged() { - if (StatusBarSimpleFragment.isEnabled()) { + if (StatusBarRootModernization.isEnabled()) { return; } updateStatusBarVisibilities(/* animate= */ true); @@ -536,7 +536,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue @Override public void onTransitionFromLockscreenToDreamStarted() { - if (StatusBarSimpleFragment.isEnabled()) { + if (StatusBarRootModernization.isEnabled()) { return; } mTransitionFromLockscreenToDreamStarted = true; @@ -547,7 +547,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue boolean hasPrimaryOngoingActivity, boolean hasSecondaryOngoingActivity, boolean shouldAnimate) { - if (StatusBarSimpleFragment.isEnabled()) { + if (StatusBarRootModernization.isEnabled()) { return; } mHasPrimaryOngoingActivity = hasPrimaryOngoingActivity; @@ -558,7 +558,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue @Override public void onIsHomeStatusBarAllowedBySceneChanged( boolean isHomeStatusBarAllowedByScene) { - if (StatusBarSimpleFragment.isEnabled()) { + if (StatusBarRootModernization.isEnabled()) { return; } mHomeStatusBarAllowedByScene = isHomeStatusBarAllowedByScene; @@ -568,7 +568,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue @Override public void disable(int displayId, int state1, int state2, boolean animate) { - if (StatusBarSimpleFragment.isEnabled()) { + if (StatusBarRootModernization.isEnabled()) { return; } if (displayId != getContext().getDisplayId()) { @@ -582,7 +582,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } private void updateStatusBarVisibilities(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); StatusBarVisibilityModel previousModel = mLastModifiedVisibility; StatusBarVisibilityModel newModel = calculateInternalModel(mLastSystemVisibility); @@ -623,7 +623,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private StatusBarVisibilityModel calculateInternalModel( StatusBarVisibilityModel externalModel) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); // TODO(b/328393714) use HeadsUpNotificationInteractor.showHeadsUpStatusBar instead. boolean headsUpVisible = @@ -677,7 +677,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue * mLastModifiedVisibility. */ private void updateNotificationIconAreaAndOngoingActivityChip(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); StatusBarVisibilityModel visibilityModel = mLastModifiedVisibility; boolean disableNotifications = !visibilityModel.getShowNotificationIcons(); @@ -714,7 +714,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } private boolean shouldHideStatusBar() { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); boolean isDefaultDisplay = getContext().getDisplayId() == Display.DEFAULT_DISPLAY; boolean shouldHideForCurrentDisplay = @@ -776,7 +776,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } private void hideEndSideContent(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); if (!animate || !mAnimationsEnabled) { mEndSideAlphaController.setAlpha(/*alpha*/ 0f, SOURCE_OTHER); } else { @@ -786,7 +786,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } private void showEndSideContent(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); if (!animate || !mAnimationsEnabled) { mEndSideAlphaController.setAlpha(1f, SOURCE_OTHER); return; @@ -803,18 +803,18 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } private void hideClock(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateHiddenState(mClockView, clockHiddenMode(), animate); } private void showClock(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateShow(mClockView, animate); } /** Hides the primary ongoing activity chip. */ private void hidePrimaryOngoingActivityChip(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateHiddenState(mPrimaryOngoingActivityChip, View.GONE, animate); } @@ -826,18 +826,18 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue * activities. See b/332662551. */ private void showPrimaryOngoingActivityChip(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateShow(mPrimaryOngoingActivityChip, animate); } private void hideSecondaryOngoingActivityChip(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateHiddenState(mSecondaryOngoingActivityChip, View.GONE, animate); } private void showSecondaryOngoingActivityChip(boolean animate) { StatusBarNotifChips.assertInNewMode(); - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateShow(mSecondaryOngoingActivityChip, animate); } @@ -846,7 +846,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue * don't set the clock GONE otherwise it'll mess up the animation. */ private int clockHiddenMode() { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); if (!mShadeExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing() && !mStatusBarStateController.isDozing()) { return View.INVISIBLE; @@ -855,24 +855,24 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } public void hideNotificationIconArea(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateHide(mNotificationIconAreaInner, animate); } public void showNotificationIconArea(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateShow(mNotificationIconAreaInner, animate); } public void hideOperatorName(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); if (mOperatorNameViewController != null) { animateHide(mOperatorNameViewController.getView(), animate); } } public void showOperatorName(boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); if (mOperatorNameViewController != null) { animateShow(mOperatorNameViewController.getView(), animate); } @@ -882,7 +882,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue * Animate a view to INVISIBLE or GONE */ private void animateHiddenState(final View v, int state, boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); v.animate().cancel(); if (!animate || !mAnimationsEnabled) { v.setAlpha(0f); @@ -902,7 +902,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue * Hides a view. */ private void animateHide(final View v, boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); animateHiddenState(v, View.INVISIBLE, animate); } @@ -910,7 +910,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue * Shows a view, and synchronizes the animation with Keyguard exit animations, if applicable. */ private void animateShow(View v, boolean animate) { - StatusBarSimpleFragment.assertInLegacyMode(); + StatusBarRootModernization.assertInLegacyMode(); v.animate().cancel(); v.setVisibility(View.VISIBLE); if (!animate || !mAnimationsEnabled) { @@ -949,7 +949,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mOperatorNameViewController.init(); // This view should not be visible on lock-screen if (mKeyguardStateController.isShowing()) { - if (!StatusBarSimpleFragment.isEnabled()) { + if (!StatusBarRootModernization.isEnabled()) { hideOperatorName(false); } } @@ -957,7 +957,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } private void initOngoingCallChip() { - if (!StatusBarSimpleFragment.isEnabled()) { + if (!StatusBarRootModernization.isEnabled()) { mOngoingCallController.addCallback(mOngoingCallListener); } // TODO(b/364653005): Do we also need to set the secondary activity chip? @@ -969,7 +969,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue @Override public void onDozingChanged(boolean isDozing) { - if (StatusBarSimpleFragment.isEnabled()) { + if (StatusBarRootModernization.isEnabled()) { return; } updateStatusBarVisibilities(/* animate= */ false); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt index eaf15a8cbe17..e4929b574bd1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt @@ -20,7 +20,7 @@ import android.view.View import androidx.core.animation.Interpolator import androidx.core.animation.ValueAnimator import com.android.app.animation.InterpolatorsAndroidX -import com.android.systemui.statusbar.core.StatusBarSimpleFragment +import com.android.systemui.statusbar.core.StatusBarRootModernization /** * A controller that keeps track of multiple sources applying alpha value changes to a view. It will @@ -75,7 +75,7 @@ constructor(private val view: View, private val initialAlpha: Float = 1f) { private fun applyAlphaToView() { val minAlpha = getMinAlpha() - if (!StatusBarSimpleFragment.isEnabled) { + if (!StatusBarRootModernization.isEnabled) { view.visibility = if (minAlpha != 0f) View.VISIBLE else View.INVISIBLE view.alpha = minAlpha } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt index 9cbfc440ab16..94e9d26c9dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt @@ -21,6 +21,7 @@ import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager +import android.telephony.satellite.NtnSignalStrength import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.systemui.dagger.SysUISingleton @@ -31,11 +32,7 @@ import javax.inject.Inject /** Logs for inputs into the mobile pipeline. */ @SysUISingleton -class MobileInputLogger -@Inject -constructor( - @MobileInputLog private val buffer: LogBuffer, -) { +class MobileInputLogger @Inject constructor(@MobileInputLog private val buffer: LogBuffer) { fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) { buffer.log( TAG, @@ -49,7 +46,7 @@ constructor( { "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" + " operator=$str1" - } + }, ) } @@ -61,7 +58,7 @@ constructor( int1 = subId bool1 = serviceState.isEmergencyOnly }, - { "ACTION_SERVICE_STATE for subId=$int1. ServiceState.isEmergencyOnly=$bool1" } + { "ACTION_SERVICE_STATE for subId=$int1. ServiceState.isEmergencyOnly=$bool1" }, ) } @@ -70,7 +67,7 @@ constructor( TAG, LogLevel.INFO, { int1 = subId }, - { "ACTION_SERVICE_STATE for subId=$int1. Intent is missing extras. Ignoring" } + { "ACTION_SERVICE_STATE for subId=$int1. Intent is missing extras. Ignoring" }, ) } @@ -82,7 +79,16 @@ constructor( int1 = subId str1 = signalStrength.toString() }, - { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" } + { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" }, + ) + } + + fun logNtnSignalStrengthChanged(signalStrength: NtnSignalStrength) { + buffer.log( + TAG, + LogLevel.INFO, + { int1 = signalStrength.level }, + { "onCarrierRoamingNtnSignalStrengthChanged: level=$int1" }, ) } @@ -128,7 +134,7 @@ constructor( TAG, LogLevel.INFO, { bool1 = active }, - { "onCarrierRoamingNtnModeChanged: $bool1" } + { "onCarrierRoamingNtnModeChanged: $bool1" }, ) } @@ -146,12 +152,7 @@ constructor( } fun logCarrierConfigChanged(subId: Int) { - buffer.log( - TAG, - LogLevel.INFO, - { int1 = subId }, - { "onCarrierConfigChanged: subId=$int1" }, - ) + buffer.log(TAG, LogLevel.INFO, { int1 = subId }, { "onCarrierConfigChanged: subId=$int1" }) } fun logOnDataEnabledChanged(enabled: Boolean, subId: Int) { @@ -175,7 +176,7 @@ constructor( TAG, LogLevel.INFO, { str1 = config.toString() }, - { "defaultDataSubRatConfig: $str1" } + { "defaultDataSubRatConfig: $str1" }, ) } @@ -184,7 +185,7 @@ constructor( TAG, LogLevel.INFO, { str1 = mapping.toString() }, - { "defaultMobileIconMapping: $str1" } + { "defaultMobileIconMapping: $str1" }, ) } @@ -216,7 +217,7 @@ constructor( { "Intent: ACTION_SERVICE_PROVIDERS_UPDATED." + " showSpn=$bool1 spn=$str1 dataSpn=$str2 showPlmn=$bool2 plmn=$str3" - } + }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 205205eac210..07843f1ef041 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -107,6 +107,12 @@ interface MobileConnectionRepository { // @IntRange(from = 0, to = 4) val primaryLevel: StateFlow<Int> + /** + * This level can be used to reflect the signal strength when in carrier roaming NTN mode + * (carrier-based satellite) + */ + val satelliteLevel: StateFlow<Int> + /** The current data connection state. See [DataConnectionState] */ val dataConnectionState: StateFlow<DataConnectionState> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt index 3261b71ece3c..be3977ecd4ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt @@ -37,12 +37,14 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullM import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_SATELLITE_LEVEL import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -75,7 +77,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = "inflate", - _inflateSignalStrength.value + _inflateSignalStrength.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _inflateSignalStrength.value) @@ -89,7 +91,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_EMERGENCY, - _isEmergencyOnly.value + _isEmergencyOnly.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isEmergencyOnly.value) @@ -100,7 +102,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_ROAMING, - _isRoaming.value + _isRoaming.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isRoaming.value) @@ -111,7 +113,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_OPERATOR, - _operatorAlphaShort.value + _operatorAlphaShort.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _operatorAlphaShort.value) @@ -122,7 +124,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_IN_SERVICE, - _isInService.value + _isInService.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value) @@ -133,7 +135,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_NTN, - _isNonTerrestrial.value + _isNonTerrestrial.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isNonTerrestrial.value) @@ -144,7 +146,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_GSM, - _isGsm.value + _isGsm.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isGsm.value) @@ -155,7 +157,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_CDMA_LEVEL, - _cdmaLevel.value + _cdmaLevel.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _cdmaLevel.value) @@ -166,10 +168,21 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_PRIMARY_LEVEL, - _primaryLevel.value + _primaryLevel.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _primaryLevel.value) + private val _satelliteLevel = MutableStateFlow(0) + override val satelliteLevel: StateFlow<Int> = + _satelliteLevel + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_SATELLITE_LEVEL, + _satelliteLevel.value, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _satelliteLevel.value) + private val _dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected) override val dataConnectionState = _dataConnectionState @@ -177,12 +190,7 @@ class DemoMobileConnectionRepository( .stateIn(scope, SharingStarted.WhileSubscribed(), _dataConnectionState.value) private val _dataActivityDirection = - MutableStateFlow( - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ) - ) + MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) override val dataActivityDirection = _dataActivityDirection .logDiffsForTable(tableLogBuffer, columnPrefix = "", _dataActivityDirection.value) @@ -195,7 +203,7 @@ class DemoMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_CARRIER_NETWORK_CHANGE, - _carrierNetworkChangeActive.value + _carrierNetworkChangeActive.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierNetworkChangeActive.value) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt index 2e4767893c3d..75f613d7e3c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -90,7 +90,7 @@ class CarrierMergedConnectionRepository( TAG, "Connection repo subId=$subId " + "does not equal wifi repo subId=${network.subscriptionId}; " + - "not showing carrier merged" + "not showing carrier merged", ) null } @@ -149,7 +149,7 @@ class CarrierMergedConnectionRepository( .stateIn( scope, SharingStarted.WhileSubscribed(), - ResolvedNetworkType.UnknownNetworkType + ResolvedNetworkType.UnknownNetworkType, ) override val dataConnectionState = @@ -173,6 +173,7 @@ class CarrierMergedConnectionRepository( override val isNonTerrestrial = MutableStateFlow(false).asStateFlow() override val isGsm = MutableStateFlow(false).asStateFlow() override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow() + override val satelliteLevel = MutableStateFlow(0) /** * Carrier merged connections happen over wifi but are displayed as a mobile triangle. Because @@ -207,10 +208,7 @@ class CarrierMergedConnectionRepository( @Application private val scope: CoroutineScope, private val wifiRepository: WifiRepository, ) { - fun build( - subId: Int, - mobileLogger: TableLogBuffer, - ): MobileConnectionRepository { + fun build(subId: Int, mobileLogger: TableLogBuffer): MobileConnectionRepository { return CarrierMergedConnectionRepository( subId, mobileLogger, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt index a5e47a6e68cd..fae9be083e12 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -132,12 +132,12 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_EMERGENCY, - activeRepo.value.isEmergencyOnly.value + activeRepo.value.isEmergencyOnly.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.isEmergencyOnly.value + activeRepo.value.isEmergencyOnly.value, ) override val isRoaming = @@ -147,7 +147,7 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_ROAMING, - activeRepo.value.isRoaming.value + activeRepo.value.isRoaming.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isRoaming.value) @@ -158,12 +158,12 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_OPERATOR, - activeRepo.value.operatorAlphaShort.value + activeRepo.value.operatorAlphaShort.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.operatorAlphaShort.value + activeRepo.value.operatorAlphaShort.value, ) override val isInService = @@ -173,7 +173,7 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_IN_SERVICE, - activeRepo.value.isInService.value + activeRepo.value.isInService.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value) @@ -184,12 +184,12 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_NTN, - activeRepo.value.isNonTerrestrial.value + activeRepo.value.isNonTerrestrial.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.isNonTerrestrial.value + activeRepo.value.isNonTerrestrial.value, ) override val isGsm = @@ -199,7 +199,7 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_IS_GSM, - activeRepo.value.isGsm.value + activeRepo.value.isGsm.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isGsm.value) @@ -210,7 +210,7 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_CDMA_LEVEL, - activeRepo.value.cdmaLevel.value + activeRepo.value.cdmaLevel.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaLevel.value) @@ -221,22 +221,33 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_PRIMARY_LEVEL, - activeRepo.value.primaryLevel.value + activeRepo.value.primaryLevel.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.primaryLevel.value) + override val satelliteLevel: StateFlow<Int> = + activeRepo + .flatMapLatest { it.satelliteLevel } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_SATELLITE_LEVEL, + activeRepo.value.satelliteLevel.value, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.satelliteLevel.value) + override val dataConnectionState = activeRepo .flatMapLatest { it.dataConnectionState } .logDiffsForTable( tableLogBuffer, columnPrefix = "", - activeRepo.value.dataConnectionState.value + activeRepo.value.dataConnectionState.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.dataConnectionState.value + activeRepo.value.dataConnectionState.value, ) override val dataActivityDirection = @@ -245,12 +256,12 @@ class FullMobileConnectionRepository( .logDiffsForTable( tableLogBuffer, columnPrefix = "", - activeRepo.value.dataActivityDirection.value + activeRepo.value.dataActivityDirection.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.dataActivityDirection.value + activeRepo.value.dataActivityDirection.value, ) override val carrierNetworkChangeActive = @@ -260,12 +271,12 @@ class FullMobileConnectionRepository( tableLogBuffer, columnPrefix = "", columnName = COL_CARRIER_NETWORK_CHANGE, - activeRepo.value.carrierNetworkChangeActive.value + activeRepo.value.carrierNetworkChangeActive.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.carrierNetworkChangeActive.value + activeRepo.value.carrierNetworkChangeActive.value, ) override val resolvedNetworkType = @@ -274,12 +285,12 @@ class FullMobileConnectionRepository( .logDiffsForTable( tableLogBuffer, columnPrefix = "", - activeRepo.value.resolvedNetworkType.value + activeRepo.value.resolvedNetworkType.value, ) .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.resolvedNetworkType.value + activeRepo.value.resolvedNetworkType.value, ) override val dataEnabled = @@ -305,7 +316,7 @@ class FullMobileConnectionRepository( .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.inflateSignalStrength.value + activeRepo.value.inflateSignalStrength.value, ) override val allowNetworkSliceIndicator = @@ -320,7 +331,7 @@ class FullMobileConnectionRepository( .stateIn( scope, SharingStarted.WhileSubscribed(), - activeRepo.value.allowNetworkSliceIndicator.value + activeRepo.value.allowNetworkSliceIndicator.value, ) override val numberOfLevels = @@ -439,6 +450,7 @@ class FullMobileConnectionRepository( const val COL_IS_IN_SERVICE = "isInService" const val COL_OPERATOR = "operatorName" const val COL_PRIMARY_LEVEL = "primaryLevel" + const val COL_SATELLITE_LEVEL = "satelliteLevel" const val COL_ROAMING = "roaming" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 62bd8ad4317c..8a1e7f9a0096 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -41,6 +41,7 @@ import android.telephony.TelephonyManager.ERI_ON import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID +import android.telephony.satellite.NtnSignalStrength import com.android.settingslib.Utils import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -173,7 +174,7 @@ class MobileConnectionRepositoryImpl( override fun onDataConnectionStateChanged( dataState: Int, - networkType: Int + networkType: Int, ) { logger.logOnDataConnectionStateChanged(dataState, networkType, subId) trySend(CallbackEvent.OnDataConnectionStateChanged(dataState)) @@ -195,6 +196,17 @@ class MobileConnectionRepositoryImpl( logger.logOnSignalStrengthsChanged(signalStrength, subId) trySend(CallbackEvent.OnSignalStrengthChanged(signalStrength)) } + + override fun onCarrierRoamingNtnSignalStrengthChanged( + signalStrength: NtnSignalStrength + ) { + logger.logNtnSignalStrengthChanged(signalStrength) + trySend( + CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged( + signalStrength + ) + ) + } } telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } @@ -267,6 +279,12 @@ class MobileConnectionRepositoryImpl( .map { it.signalStrength.level } .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val satelliteLevel: StateFlow<Int> = + callbackEvents + .mapNotNull { it.onCarrierRoamingNtnSignalStrengthChanged } + .map { it.signalStrength.level } + .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + override val dataConnectionState = callbackEvents .mapNotNull { it.onDataConnectionStateChanged } @@ -280,7 +298,7 @@ class MobileConnectionRepositoryImpl( .stateIn( scope, SharingStarted.WhileSubscribed(), - DataActivityModel(hasActivityIn = false, hasActivityOut = false) + DataActivityModel(hasActivityIn = false, hasActivityOut = false), ) override val carrierNetworkChangeActive = @@ -385,7 +403,7 @@ class MobileConnectionRepositoryImpl( if ( intent.getIntExtra( EXTRA_SUBSCRIPTION_INDEX, - INVALID_SUBSCRIPTION_ID + INVALID_SUBSCRIPTION_ID, ) == subId ) { logger.logServiceProvidersUpdatedBroadcast(intent) @@ -399,7 +417,7 @@ class MobileConnectionRepositoryImpl( context.registerReceiver( receiver, - IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED) + IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED), ) awaitClose { context.unregisterReceiver(receiver) } @@ -524,6 +542,9 @@ sealed interface CallbackEvent { data class OnServiceStateChanged(val serviceState: ServiceState) : CallbackEvent data class OnSignalStrengthChanged(val signalStrength: SignalStrength) : CallbackEvent + + data class OnCarrierRoamingNtnSignalStrengthChanged(val signalStrength: NtnSignalStrength) : + CallbackEvent } /** @@ -539,6 +560,9 @@ data class TelephonyCallbackState( val onDisplayInfoChanged: CallbackEvent.OnDisplayInfoChanged? = null, val onServiceStateChanged: CallbackEvent.OnServiceStateChanged? = null, val onSignalStrengthChanged: CallbackEvent.OnSignalStrengthChanged? = null, + val onCarrierRoamingNtnSignalStrengthChanged: + CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged? = + null, ) { fun applyEvent(event: CallbackEvent): TelephonyCallbackState { return when (event) { @@ -555,6 +579,8 @@ data class TelephonyCallbackState( copy(onServiceStateChanged = event) } is CallbackEvent.OnSignalStrengthChanged -> copy(onSignalStrengthChanged = event) + is CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged -> + copy(onCarrierRoamingNtnSignalStrengthChanged = event) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 4ef328cf1623..1bf14af7ea6e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -335,7 +335,11 @@ class MobileIconInteractorImpl( // Satellite level is unaffected by the inflateSignalStrength property // See b/346904529 for details private val satelliteShownLevel: StateFlow<Int> = - combine(level, isInService) { level, isInService -> if (isInService) level else 0 } + if (Flags.carrierRoamingNbIotNtn()) { + connectionRepository.satelliteLevel + } else { + combine(level, isInService) { level, isInService -> if (isInService) level else 0 } + } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) private val cellularIcon: Flow<SignalIconModel.Cellular> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 11d73397ca22..2177e025b976 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -22,6 +22,7 @@ import android.view.View import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.repeatWhenAttached @@ -30,13 +31,12 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel -import com.android.systemui.statusbar.core.StatusBarSimpleFragment +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel import javax.inject.Inject -import com.android.app.tracing.coroutines.launchTraced as launch /** * Interface to assist with binding the [CollapsedStatusBarFragment] to [HomeStatusBarViewModel]. @@ -91,7 +91,7 @@ class HomeStatusBarViewBinderImpl @Inject constructor() : HomeStatusBarViewBinde launch { viewModel.primaryOngoingActivityChip.collect { primaryChipModel -> OngoingActivityChipBinder.bind(primaryChipModel, primaryChipView) - if (StatusBarSimpleFragment.isEnabled) { + if (StatusBarRootModernization.isEnabled) { when (primaryChipModel) { is OngoingActivityChipModel.Shown -> primaryChipView.show(shouldAnimateChange = true) @@ -133,7 +133,7 @@ class HomeStatusBarViewBinderImpl @Inject constructor() : HomeStatusBarViewBinde // enough space for it. OngoingActivityChipBinder.bind(chips.secondary, secondaryChipView) - if (StatusBarSimpleFragment.isEnabled) { + if (StatusBarRootModernization.isEnabled) { primaryChipView.adjustVisibility(chips.primary.toVisibilityModel()) secondaryChipView.adjustVisibility( chips.secondary.toVisibilityModel() @@ -160,7 +160,7 @@ class HomeStatusBarViewBinderImpl @Inject constructor() : HomeStatusBarViewBinde } } - if (StatusBarSimpleFragment.isEnabled) { + if (StatusBarRootModernization.isEnabled) { val clockView = view.requireViewById<View>(R.id.clock) launch { viewModel.isClockVisible.collect { clockView.adjustVisibility(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index 247abc3da175..a9c2f72f7b18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -104,7 +104,7 @@ fun StatusBarRoot( darkIconDispatcher: DarkIconDispatcher, onViewCreated: (ViewGroup) -> Unit, ) { - // None of these methods are used when [StatusBarSimpleFragment] is on. + // None of these methods are used when [StatusBarRootModernization] is on. // This can be deleted once the fragment is gone val nopVisibilityChangeListener = object : StatusBarVisibilityChangeListener { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index 76024cd565d1..c7b6be3fc4ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -17,12 +17,15 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod import android.annotation.SuppressLint +import android.content.Context import android.net.wifi.ScanResult import android.net.wifi.WifiManager +import android.os.UserHandle import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.Flags.multiuserWifiPickerTrackerSupport import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -43,6 +46,7 @@ import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiReposito import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Unavailable.toHotspotDeviceType import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry +import com.android.systemui.user.data.repository.UserRepository import com.android.wifitrackerlib.HotspotNetworkEntry import com.android.wifitrackerlib.MergedCarrierEntry import com.android.wifitrackerlib.WifiEntry @@ -53,10 +57,12 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -68,6 +74,8 @@ import kotlinx.coroutines.flow.stateIn class WifiRepositoryImpl @Inject constructor( + @Application applicationContext: Context, + private val userRepository: UserRepository, @Application private val scope: CoroutineScope, @Main private val mainExecutor: Executor, @Background private val bgDispatcher: CoroutineDispatcher, @@ -84,90 +92,226 @@ constructor( private var wifiPickerTracker: WifiPickerTracker? = null - private val wifiPickerTrackerInfo: StateFlow<WifiPickerTrackerInfo> = run { - var current = - WifiPickerTrackerInfo( - state = WIFI_STATE_DEFAULT, - isDefault = false, - primaryNetwork = WIFI_NETWORK_DEFAULT, - secondaryNetworks = emptyList(), - ) - callbackFlow { - val callback = - object : WifiPickerTracker.WifiPickerTrackerCallback { - override fun onWifiEntriesChanged() { - val connectedEntry = wifiPickerTracker.mergedOrPrimaryConnection - logOnWifiEntriesChanged(connectedEntry) - - val activeNetworks = wifiPickerTracker?.activeWifiEntries ?: emptyList() - val secondaryNetworks = - activeNetworks - .filter { it != connectedEntry && !it.isPrimaryNetwork } - .map { it.toWifiNetworkModel() } - - // [WifiPickerTracker.connectedWifiEntry] will return the same instance - // but with updated internals. For example, when its validation status - // changes from false to true, the same instance is re-used but with the - // validated field updated. - // - // Because it's the same instance, the flow won't re-emit the value - // (even though the internals have changed). So, we need to transform it - // into our internal model immediately. [toWifiNetworkModel] always - // returns a new instance, so the flow is guaranteed to emit. - send( - newPrimaryNetwork = - connectedEntry?.toPrimaryWifiNetworkModel() - ?: WIFI_NETWORK_DEFAULT, - newSecondaryNetworks = secondaryNetworks, - newIsDefault = connectedEntry?.isDefaultNetwork ?: false, - ) - } + @VisibleForTesting + val selectedUserContext: Flow<Context> = + userRepository.selectedUserInfo.map { + applicationContext.createContextAsUser(UserHandle.of(it.id), /* flags= */ 0) + } - override fun onWifiStateChanged() { - val state = wifiPickerTracker?.wifiState - logOnWifiStateChanged(state) - send(newState = state ?: WIFI_STATE_DEFAULT) - } + var current = + WifiPickerTrackerInfo( + state = WIFI_STATE_DEFAULT, + isDefault = false, + primaryNetwork = WIFI_NETWORK_DEFAULT, + secondaryNetworks = emptyList(), + ) - override fun onNumSavedNetworksChanged() {} - - override fun onNumSavedSubscriptionsChanged() {} - - private fun send( - newState: Int = current.state, - newIsDefault: Boolean = current.isDefault, - newPrimaryNetwork: WifiNetworkModel = current.primaryNetwork, - newSecondaryNetworks: List<WifiNetworkModel> = - current.secondaryNetworks, - ) { - val new = - WifiPickerTrackerInfo( - newState, - newIsDefault, - newPrimaryNetwork, - newSecondaryNetworks, - ) - current = new - trySend(new) + @kotlinx.coroutines.ExperimentalCoroutinesApi + private val wifiPickerTrackerInfo: StateFlow<WifiPickerTrackerInfo> = + if (multiuserWifiPickerTrackerSupport()) { + selectedUserContext + .flatMapLatest { currentContext + -> // flatMapLatest because when selectedUserContext emits a new value, we want + // to + // re-create a whole flow + callbackFlow { + val callback = + object : WifiPickerTracker.WifiPickerTrackerCallback { + override fun onWifiEntriesChanged() { + val connectedEntry = + wifiPickerTracker.mergedOrPrimaryConnection + logOnWifiEntriesChanged(connectedEntry) + + val activeNetworks = + wifiPickerTracker?.activeWifiEntries ?: emptyList() + val secondaryNetworks = + activeNetworks + .filter { + it != connectedEntry && !it.isPrimaryNetwork + } + .map { it.toWifiNetworkModel() } + + // [WifiPickerTracker.connectedWifiEntry] will return the + // same + // instance but with updated internals. For example, when + // its + // validation status changes from false to true, the same + // instance is re-used but with the validated field updated. + // + // Because it's the same instance, the flow won't re-emit + // the + // value (even though the internals have changed). So, we + // need + // to transform it into our internal model immediately. + // [toWifiNetworkModel] always returns a new instance, so + // the + // flow is guaranteed to emit. + send( + newPrimaryNetwork = + connectedEntry?.toPrimaryWifiNetworkModel() + ?: WIFI_NETWORK_DEFAULT, + newSecondaryNetworks = secondaryNetworks, + newIsDefault = connectedEntry?.isDefaultNetwork ?: false, + ) + } + + override fun onWifiStateChanged() { + val state = wifiPickerTracker?.wifiState + logOnWifiStateChanged(state) + send(newState = state ?: WIFI_STATE_DEFAULT) + } + + override fun onNumSavedNetworksChanged() {} + + override fun onNumSavedSubscriptionsChanged() {} + + private fun send( + newState: Int = current.state, + newIsDefault: Boolean = current.isDefault, + newPrimaryNetwork: WifiNetworkModel = + current.primaryNetwork, + newSecondaryNetworks: List<WifiNetworkModel> = + current.secondaryNetworks, + ) { + val new = + WifiPickerTrackerInfo( + newState, + newIsDefault, + newPrimaryNetwork, + newSecondaryNetworks, + ) + current = new + trySend(new) + } + } + wifiPickerTracker = + wifiPickerTrackerFactory + .create(currentContext, lifecycle, callback, "WifiRepository") + .apply { + // By default, [WifiPickerTracker] will scan to see all + // available wifi networks in the area. Because SysUI only + // needs to display the **connected** network, we don't + // need scans to be running (and in fact, running scans is + // costly and should be avoided whenever possible). + this?.disableScanning() + } + + // The lifecycle must be STARTED in order for the callback to receive + // events. + mainExecutor.execute { + lifecycle.currentState = Lifecycle.State.STARTED + } + awaitClose { + mainExecutor.execute { + lifecycle.currentState = Lifecycle.State.CREATED + } + } } - } + .stateIn(scope, SharingStarted.Eagerly, current) + } + .stateIn(scope, SharingStarted.Eagerly, current) + } else { - wifiPickerTracker = - wifiPickerTrackerFactory.create(lifecycle, callback, "WifiRepository").apply { - // By default, [WifiPickerTracker] will scan to see all available wifi - // networks in the area. Because SysUI only needs to display the - // **connected** network, we don't need scans to be running (and in fact, - // running scans is costly and should be avoided whenever possible). - this?.disableScanning() + run { + var current = + WifiPickerTrackerInfo( + state = WIFI_STATE_DEFAULT, + isDefault = false, + primaryNetwork = WIFI_NETWORK_DEFAULT, + secondaryNetworks = emptyList(), + ) + callbackFlow { + val callback = + object : WifiPickerTracker.WifiPickerTrackerCallback { + override fun onWifiEntriesChanged() { + val connectedEntry = wifiPickerTracker.mergedOrPrimaryConnection + logOnWifiEntriesChanged(connectedEntry) + + val activeNetworks = + wifiPickerTracker?.activeWifiEntries ?: emptyList() + val secondaryNetworks = + activeNetworks + .filter { it != connectedEntry && !it.isPrimaryNetwork } + .map { it.toWifiNetworkModel() } + + // [WifiPickerTracker.connectedWifiEntry] will return the same + // instance + // but with updated internals. For example, when its validation + // status + // changes from false to true, the same instance is re-used but + // with the + // validated field updated. + // + // Because it's the same instance, the flow won't re-emit the + // value + // (even though the internals have changed). So, we need to + // transform it + // into our internal model immediately. [toWifiNetworkModel] + // always + // returns a new instance, so the flow is guaranteed to emit. + send( + newPrimaryNetwork = + connectedEntry?.toPrimaryWifiNetworkModel() + ?: WIFI_NETWORK_DEFAULT, + newSecondaryNetworks = secondaryNetworks, + newIsDefault = connectedEntry?.isDefaultNetwork ?: false, + ) + } + + override fun onWifiStateChanged() { + val state = wifiPickerTracker?.wifiState + logOnWifiStateChanged(state) + send(newState = state ?: WIFI_STATE_DEFAULT) + } + + override fun onNumSavedNetworksChanged() {} + + override fun onNumSavedSubscriptionsChanged() {} + + private fun send( + newState: Int = current.state, + newIsDefault: Boolean = current.isDefault, + newPrimaryNetwork: WifiNetworkModel = current.primaryNetwork, + newSecondaryNetworks: List<WifiNetworkModel> = + current.secondaryNetworks, + ) { + val new = + WifiPickerTrackerInfo( + newState, + newIsDefault, + newPrimaryNetwork, + newSecondaryNetworks, + ) + current = new + trySend(new) + } + } + + wifiPickerTracker = + wifiPickerTrackerFactory + .create(applicationContext, lifecycle, callback, "WifiRepository") + .apply { + // By default, [WifiPickerTracker] will scan to see all + // available wifi + // networks in the area. Because SysUI only needs to display the + // **connected** network, we don't need scans to be running (and + // in fact, + // running scans is costly and should be avoided whenever + // possible). + this?.disableScanning() + } + // The lifecycle must be STARTED in order for the callback to receive + // events. + mainExecutor.execute { lifecycle.currentState = Lifecycle.State.STARTED } + awaitClose { + mainExecutor.execute { + lifecycle.currentState = Lifecycle.State.CREATED + } + } } - // The lifecycle must be STARTED in order for the callback to receive events. - mainExecutor.execute { lifecycle.currentState = Lifecycle.State.STARTED } - awaitClose { - mainExecutor.execute { lifecycle.currentState = Lifecycle.State.CREATED } - } + .stateIn(scope, SharingStarted.Eagerly, current) } - .stateIn(scope, SharingStarted.Eagerly, current) - } + } override val isWifiEnabled: StateFlow<Boolean> = wifiPickerTrackerInfo @@ -185,11 +329,7 @@ constructor( wifiPickerTrackerInfo .map { it.primaryNetwork } .distinctUntilChanged() - .logDiffsForTable( - tableLogger, - columnPrefix = "", - initialValue = WIFI_NETWORK_DEFAULT, - ) + .logDiffsForTable(tableLogger, columnPrefix = "", initialValue = WIFI_NETWORK_DEFAULT) .stateIn(scope, SharingStarted.Eagerly, WIFI_NETWORK_DEFAULT) override val secondaryNetworks: StateFlow<List<WifiNetworkModel>> = @@ -348,7 +488,7 @@ constructor( TAG, LogLevel.DEBUG, { str1 = prettyPrintActivity(activity) }, - { "onActivityChanged: $str1" } + { "onActivityChanged: $str1" }, ) } @@ -379,13 +519,15 @@ constructor( /** The currently primary wifi network. */ val primaryNetwork: WifiNetworkModel, /** The current secondary network(s), if any. Specifically excludes the primary network. */ - val secondaryNetworks: List<WifiNetworkModel> + val secondaryNetworks: List<WifiNetworkModel>, ) @SysUISingleton class Factory @Inject constructor( + @Application private val applicationContext: Context, + private val userRepository: UserRepository, @Application private val scope: CoroutineScope, @Main private val mainExecutor: Executor, @Background private val bgDispatcher: CoroutineDispatcher, @@ -395,6 +537,8 @@ constructor( ) { fun create(wifiManager: WifiManager): WifiRepositoryImpl { return WifiRepositoryImpl( + applicationContext, + userRepository, scope, mainExecutor, bgDispatcher, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index c7bd5a1bb9a8..9187e3c43380 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -206,12 +206,14 @@ public interface StatusBarPolicyModule { @SysUISingleton @Provides static AccessPointControllerImpl provideAccessPointControllerImpl( + @Application Context context, UserManager userManager, UserTracker userTracker, @Main Executor mainExecutor, WifiPickerTrackerFactory wifiPickerTrackerFactory ) { AccessPointControllerImpl controller = new AccessPointControllerImpl( + context, userManager, userTracker, mainExecutor, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java index 3f6ef16e2e5e..b79d39e65682 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java @@ -35,7 +35,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.compose.ComposeInitializer; -import com.android.systemui.statusbar.core.StatusBarSimpleFragment; +import com.android.systemui.statusbar.core.StatusBarRootModernization; import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController; /** @@ -64,7 +64,7 @@ public class StatusBarWindowView extends FrameLayout { public void onAttachedToWindow() { super.onAttachedToWindow(); - if (StatusBarSimpleFragment.isEnabled()) { + if (StatusBarRootModernization.isEnabled()) { ComposeInitializer.INSTANCE.onAttachedToWindow(this); } } @@ -73,7 +73,7 @@ public class StatusBarWindowView extends FrameLayout { public void onDetachedFromWindow() { super.onDetachedFromWindow(); - if (StatusBarSimpleFragment.isEnabled()) { + if (StatusBarRootModernization.isEnabled()) { ComposeInitializer.INSTANCE.onDetachedFromWindow(this); } } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index f20ce63467f7..ad97b21ea60b 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -23,11 +23,13 @@ import android.app.admin.DevicePolicyManager import android.content.Context import android.content.IntentFilter import android.content.pm.UserInfo +import android.content.res.Resources import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.internal.statusbar.IStatusBarService import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -109,6 +111,9 @@ interface UserRepository { /** Whether logout for secondary users is enabled by admin device policy. */ val isSecondaryUserLogoutEnabled: StateFlow<Boolean> + /** Whether logout into system user is enabled. */ + val isLogoutToSystemUserEnabled: StateFlow<Boolean> + /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */ fun refreshUsers() @@ -121,6 +126,9 @@ interface UserRepository { /** Performs logout logout for secondary users. */ suspend fun logOutSecondaryUser() + /** Performs logout into the system user. */ + suspend fun logOutToSystemUser() + /** * Returns the user ID of the "main user" of the device. This user may have access to certain * features which are limited to at most one user. There will never be more than one main user @@ -143,6 +151,7 @@ class UserRepositoryImpl @Inject constructor( @Application private val appContext: Context, + @Main private val resources: Resources, private val manager: UserManager, @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @@ -151,6 +160,7 @@ constructor( private val tracker: UserTracker, private val devicePolicyManager: DevicePolicyManager, private val broadcastDispatcher: BroadcastDispatcher, + private val statusBarService: IStatusBarService, ) : UserRepository { private val _userSwitcherSettings: StateFlow<UserSwitcherSettingsModel> = @@ -275,12 +285,34 @@ constructor( .stateIn(applicationScope, SharingStarted.Eagerly, false) @SuppressLint("MissingPermission") + override val isLogoutToSystemUserEnabled: StateFlow<Boolean> = + selectedUser + .flatMapLatestConflated { selectedUser -> + if (selectedUser.isEligibleForLogout()) { + flowOf( + resources.getBoolean(R.bool.config_userSwitchingMustGoThroughLoginScreen) + ) + } else { + flowOf(false) + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + @SuppressLint("MissingPermission") override suspend fun logOutSecondaryUser() { if (isSecondaryUserLogoutEnabled.value) { withContext(backgroundDispatcher) { devicePolicyManager.logoutUser() } } } + override suspend fun logOutToSystemUser() { + // TODO(b/377493351) : start using proper logout API once it is available. + // Using reboot is a temporary solution. + if (isLogoutToSystemUserEnabled.value) { + withContext(backgroundDispatcher) { statusBarService.reboot(false) } + } + } + @SuppressLint("MissingPermission") override fun refreshUsers() { applicationScope.launch { @@ -316,42 +348,53 @@ constructor( private suspend fun getSettings(): UserSwitcherSettingsModel { return withContext(backgroundDispatcher) { - val isSimpleUserSwitcher = - globalSettings.getInt( - SETTING_SIMPLE_USER_SWITCHER, - if ( - appContext.resources.getBoolean( - com.android.internal.R.bool.config_expandLockScreenUserSwitcher - ) - ) { - 1 - } else { - 0 - }, - ) != 0 - - val isAddUsersFromLockscreen = - globalSettings.getInt(Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0 - - val isUserSwitcherEnabled = - globalSettings.getInt( - Settings.Global.USER_SWITCHER_ENABLED, - if ( - appContext.resources.getBoolean( - com.android.internal.R.bool.config_showUserSwitcherByDefault - ) - ) { - 1 - } else { - 0 - }, - ) != 0 - - UserSwitcherSettingsModel( - isSimpleUserSwitcher = isSimpleUserSwitcher, - isAddUsersFromLockscreen = isAddUsersFromLockscreen, - isUserSwitcherEnabled = isUserSwitcherEnabled, - ) + if ( + // TODO(b/378068979): remove once login screen-specific logic + // is implemented at framework level. + appContext.resources.getBoolean(R.bool.config_userSwitchingMustGoThroughLoginScreen) + ) { + UserSwitcherSettingsModel( + isSimpleUserSwitcher = false, + isAddUsersFromLockscreen = false, + isUserSwitcherEnabled = false, + ) + } else { + val isSimpleUserSwitcher = + globalSettings.getInt( + SETTING_SIMPLE_USER_SWITCHER, + if ( + appContext.resources.getBoolean( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher + ) + ) { + 1 + } else { + 0 + }, + ) != 0 + + val isAddUsersFromLockscreen = + globalSettings.getInt(Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0 + + val isUserSwitcherEnabled = + globalSettings.getInt( + Settings.Global.USER_SWITCHER_ENABLED, + if ( + appContext.resources.getBoolean( + com.android.internal.R.bool.config_showUserSwitcherByDefault + ) + ) { + 1 + } else { + 0 + }, + ) != 0 + UserSwitcherSettingsModel( + isSimpleUserSwitcher = isSimpleUserSwitcher, + isAddUsersFromLockscreen = isAddUsersFromLockscreen, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt index a7983605eac9..bcbd679b35eb 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt @@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable import android.os.Handler import android.os.UserManager import android.provider.Settings.Global.USER_SWITCHER_ENABLED +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -40,7 +41,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext interface UserSwitcherRepository { @@ -67,6 +67,9 @@ constructor( private val showUserSwitcherForSingleUser = context.resources.getBoolean(R.bool.qs_show_user_switcher_for_single_user) + private val userSwitchingMustGoThroughLoginScreen = + context.resources.getBoolean(R.bool.config_userSwitchingMustGoThroughLoginScreen) + override val isEnabled: Flow<Boolean> = conflatedCallbackFlow { suspend fun updateState() { trySendWithFailureLogging(isUserSwitcherEnabled(), TAG) @@ -135,7 +138,13 @@ constructor( private suspend fun isUserSwitcherEnabled(): Boolean { return withContext(bgDispatcher) { - userManager.isUserSwitcherEnabled(showUserSwitcherForSingleUser) + // TODO(b/378068979): remove once login screen-specific logic + // is implemented at framework level. + if (userSwitchingMustGoThroughLoginScreen) { + false + } else { + userManager.isUserSwitcherEnabled(showUserSwitcherForSingleUser) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt index 154f1dc3e747..f2dd25fecf08 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt @@ -23,7 +23,10 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.user.data.repository.UserRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn /** Encapsulates business logic to for the logout. */ @SysUISingleton @@ -33,11 +36,22 @@ constructor( private val userRepository: UserRepository, @Application private val applicationScope: CoroutineScope, ) { - val isLogoutEnabled: StateFlow<Boolean> = userRepository.isSecondaryUserLogoutEnabled + + val isLogoutEnabled: StateFlow<Boolean> = + combine( + userRepository.isSecondaryUserLogoutEnabled, + userRepository.isLogoutToSystemUserEnabled, + Boolean::or, + ) + .stateIn(applicationScope, SharingStarted.Eagerly, false) fun logOut() { - if (userRepository.isSecondaryUserLogoutEnabled.value) { - applicationScope.launch { userRepository.logOutSecondaryUser() } + applicationScope.launch { + if (userRepository.isSecondaryUserLogoutEnabled.value) { + userRepository.logOutSecondaryUser() + } else if (userRepository.isLogoutToSystemUserEnabled.value) { + userRepository.logOutToSystemUser() + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt index 9440a9364b62..fb157958a630 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.dialog.dagger +import com.android.systemui.volume.dialog.dagger.module.VolumeDialogModule import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent @@ -28,7 +29,7 @@ import kotlinx.coroutines.CoroutineScope * [com.android.systemui.volume.dialog.VolumeDialogPlugin] and lives alongside it. */ @VolumeDialogScope -@Subcomponent(modules = []) +@Subcomponent(modules = [VolumeDialogModule::class]) interface VolumeDialogComponent { /** diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt new file mode 100644 index 000000000000..025e269b70b6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.dagger.module + +import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepository +import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepositoryImpl +import dagger.Binds +import dagger.Module + +/** Dagger module for volume dialog code in the volume package */ +@Module +interface VolumeDialogModule { + + @Binds + fun bindVolumeDialogRingerFeedbackRepository( + ringerFeedbackRepository: VolumeDialogRingerFeedbackRepositoryImpl + ): VolumeDialogRingerFeedbackRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepository.kt new file mode 100644 index 000000000000..263972b937d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepository.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.ringer.data.repository + +import android.content.Context +import com.android.systemui.Prefs +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +interface VolumeDialogRingerFeedbackRepository { + + /** gets number of shown toasts */ + suspend fun getToastCount(): Int + + /** updates number of shown toasts */ + suspend fun updateToastCount(toastCount: Int) +} + +class VolumeDialogRingerFeedbackRepositoryImpl +@Inject +constructor( + @Application private val applicationContext: Context, + @Background val backgroundDispatcher: CoroutineDispatcher, +) : VolumeDialogRingerFeedbackRepository { + + override suspend fun getToastCount(): Int = + withContext(backgroundDispatcher) { + return@withContext Prefs.getInt( + applicationContext, + Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, + 0, + ) + } + + override suspend fun updateToastCount(toastCount: Int) { + withContext(backgroundDispatcher) { + Prefs.putInt(applicationContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, toastCount + 1) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt index 281e57fec855..b83613ba4f8c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt @@ -26,6 +26,7 @@ import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.plugins.VolumeDialogController import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor +import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepository import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel import javax.inject.Inject @@ -45,6 +46,7 @@ constructor( volumeDialogStateInteractor: VolumeDialogStateInteractor, private val controller: VolumeDialogController, private val audioSystemRepository: AudioSystemRepository, + private val ringerFeedbackRepository: VolumeDialogRingerFeedbackRepository, ) { val ringerModel: Flow<VolumeDialogRingerModel> = @@ -84,4 +86,12 @@ constructor( fun scheduleTouchFeedback() { controller.scheduleTouchFeedback() } + + suspend fun getToastCount(): Int { + return ringerFeedbackRepository.getToastCount() + } + + suspend fun updateToastCount(toastCount: Int) { + ringerFeedbackRepository.updateToastCount(toastCount) + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt index d4da226152f3..e040638324ac 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt @@ -16,17 +16,23 @@ package com.android.systemui.volume.dialog.ringer.ui.viewmodel +import android.content.Context import android.media.AudioAttributes import android.media.AudioManager.RINGER_MODE_NORMAL import android.media.AudioManager.RINGER_MODE_SILENT import android.media.AudioManager.RINGER_MODE_VIBRATE import android.os.VibrationEffect +import android.widget.Toast +import com.android.internal.R as internalR +import com.android.settingslib.Utils import com.android.settingslib.volume.shared.model.RingerMode +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.volume.Events import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog +import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor import com.android.systemui.volume.dialog.ringer.domain.VolumeDialogRingerInteractor import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel import com.android.systemui.volume.dialog.shared.VolumeDialogLogger @@ -40,26 +46,37 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +private const val SHOW_RINGER_TOAST_COUNT = 12 class VolumeDialogRingerDrawerViewModel @AssistedInject constructor( + @Application private val applicationContext: Context, @VolumeDialog private val coroutineScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, private val interactor: VolumeDialogRingerInteractor, private val vibrator: VibratorHelper, private val volumeDialogLogger: VolumeDialogLogger, + private val visibilityInteractor: VolumeDialogVisibilityInteractor, ) { private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial) val ringerViewModel: StateFlow<RingerViewModelState> = combine(interactor.ringerModel, drawerState) { ringerModel, state -> + level = ringerModel.level + levelMax = ringerModel.levelMax ringerModel.toViewModel(state) } .flowOn(backgroundDispatcher) .stateIn(coroutineScope, SharingStarted.Eagerly, RingerViewModelState.Unavailable) + // Level and Maximum level of Ring Stream. + private var level = -1 + private var levelMax = -1 + // Vibration attributes. private val sonificiationVibrationAttributes = AudioAttributes.Builder() @@ -71,8 +88,10 @@ constructor( if (drawerState.value is RingerDrawerState.Open) { Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value) provideTouchFeedback(ringerMode) + maybeShowToast(ringerMode) interactor.setRingerMode(ringerMode) } + visibilityInteractor.resetDismissTimeout() drawerState.value = when (drawerState.value) { is RingerDrawerState.Initial -> { @@ -201,6 +220,46 @@ constructor( } } + private fun maybeShowToast(ringerMode: RingerMode) { + coroutineScope.launch { + val seenToastCount = interactor.getToastCount() + if (seenToastCount > SHOW_RINGER_TOAST_COUNT) { + return@launch + } + + val toastText = + when (ringerMode.value) { + RINGER_MODE_NORMAL -> { + if (level != -1 && levelMax != -1) { + applicationContext.getString( + R.string.volume_dialog_ringer_guidance_ring, + Utils.formatPercentage(level.toLong(), levelMax.toLong()), + ) + } else { + null + } + } + + RINGER_MODE_SILENT -> + applicationContext.getString( + internalR.string.volume_dialog_ringer_guidance_silent + ) + + RINGER_MODE_VIBRATE -> + applicationContext.getString( + internalR.string.volume_dialog_ringer_guidance_vibrate + ) + + else -> + applicationContext.getString( + internalR.string.volume_dialog_ringer_guidance_vibrate + ) + } + toastText?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_SHORT).show() } + interactor.updateToastCount(seenToastCount) + } + } + @AssistedFactory interface Factory { fun create(): VolumeDialogRingerDrawerViewModel diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java index 53e6b4f82b7e..761993b3cda7 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java @@ -330,13 +330,19 @@ public class WalletScreenController implements QAWalletCardViewInfo(Context context, WalletCard walletCard) { mWalletCard = walletCard; Icon cardImageIcon = mWalletCard.getCardImage(); - if (cardImageIcon.getType() == Icon.TYPE_URI) { - mCardDrawable = null; - } else { + if (cardImageIcon.getType() == Icon.TYPE_BITMAP + || cardImageIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { mCardDrawable = mWalletCard.getCardImage().loadDrawable(context); + } else { + mCardDrawable = null; } Icon icon = mWalletCard.getCardIcon(); - mIconDrawable = icon == null ? null : icon.loadDrawable(context); + if (icon != null && (icon.getType() == Icon.TYPE_BITMAP + || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) { + mIconDrawable = icon.loadDrawable(context); + } else { + mIconDrawable = null; + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java index 1a399341f12c..ca9b866e2d18 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java @@ -283,6 +283,11 @@ public class WalletView extends FrameLayout implements WalletCardCarousel.OnCard return mCardLabel; } + @VisibleForTesting + ImageView getIcon() { + return mIcon; + } + @Nullable private static Drawable getHeaderIcon(Context context, WalletCardViewInfo walletCard) { Drawable icon = walletCard.getIcon(); diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 8039e00159f0..073781e6101d 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -271,6 +271,12 @@ public final class WMShell implements // No op. } }, mSysUiMainExecutor); + pip.addOnIsInPipStateChangedListener((isInPip) -> { + if (!isInPip) { + mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, false) + .commitUpdate(mDisplayTracker.getDefaultDisplayId()); + } + }); mSysUiState.addCallback(sysUiStateFlag -> { mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0; pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 2b167e4c5da4..65b62737b692 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -56,6 +56,7 @@ import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.plugins.clocks.ZenData.ZenMode import com.android.systemui.res.R +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ZenModeController @@ -128,6 +129,7 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var largeClockEvents: ClockFaceEvents @Mock private lateinit var parentView: View @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor + @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var zenModeController: ZenModeController private var zenModeControllerCallback: ZenModeController.Callback? = null @@ -153,6 +155,7 @@ class ClockEventControllerTest : SysuiTestCase() { .thenReturn(ClockFaceConfig(tickRate = ClockTickRate.PER_MINUTE)) whenever(smallClockController.theme).thenReturn(ThemeConfig(true, null)) whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null)) + whenever(userTracker.userId).thenReturn(1) zenModeRepository.addMode(MANUAL_DND_INACTIVE) @@ -177,6 +180,7 @@ class ClockEventControllerTest : SysuiTestCase() { withDeps.featureFlags, zenModeController, kosmos.zenModeInteractor, + userTracker, ) underTest.clock = clock diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java index 96f4a60271d2..b4c69529741e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -46,7 +46,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -222,7 +221,6 @@ public class DozeTriggersTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_NOTIFICATION_PULSING_FIX) public void testOnNotification_alreadyPulsing_notificationNotSuppressed() { // GIVEN device is pulsing Runnable pulseSuppressListener = mock(Runnable.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index fb376ce3ca40..3ddd4b58211d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -289,6 +289,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa } verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) mediaControllerFactory.setControllerForToken(session.sessionToken, controller) + whenever(controller.sessionToken).thenReturn(session.sessionToken) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) whenever(controller.metadata).thenReturn(metadataBuilder.build()) @@ -1599,6 +1600,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testTooManyCompactActions_isTruncated() { // GIVEN a notification where too many compact actions were specified @@ -1635,6 +1637,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .isEqualTo(LegacyMediaDataManagerImpl.MAX_COMPACT_ACTIONS) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testTooManyNotificationActions_isTruncated() { // GIVEN a notification where too many notification actions are added @@ -1670,6 +1673,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .isEqualTo(LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_noState_usesNotification() { val desc = "Notification Action" @@ -1703,6 +1707,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_hasPrevNext() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") @@ -1746,6 +1751,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1]) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_noPrevNext_usesCustom() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") @@ -1778,6 +1784,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3]) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_connecting() { val stateActions = PlaybackState.ACTION_PLAY @@ -1797,6 +1804,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .isEqualTo(context.getString(R.string.controls_media_button_connecting)) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_reservedSpace() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") @@ -1835,6 +1843,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(actions.reservePrev).isTrue() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_playPause_hasButton() { val stateActions = PlaybackState.ACTION_PLAY_PAUSE @@ -1998,6 +2007,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(mediaDataCaptor.value.semanticActions).isNotNull() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackStateNull_Pause_keyExists_callsListener() { whenever(controller.playbackState).thenReturn(null) @@ -2056,6 +2066,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(mediaDataCaptor.value.isClearable).isFalse() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_notifRemoved_setToResume() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) @@ -2086,6 +2097,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa ) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) @@ -2104,6 +2116,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 7d364bd832f2..e5483c0980c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -103,7 +103,6 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify @@ -113,6 +112,7 @@ import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.any import org.mockito.kotlin.capture import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import platform.test.runner.parameterized.ParameterizedAndroidJunit4 @@ -312,6 +312,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { } verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) mediaControllerFactory.setControllerForToken(session.sessionToken, controller) + whenever(controller.sessionToken).thenReturn(session.sessionToken) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) whenever(controller.metadata).thenReturn(metadataBuilder.build()) @@ -596,7 +597,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { fun testOnNotificationAdded_emptyTitle_hasPlaceholder() { // When the manager has a notification with an empty title, and the app is not // required to include a non-empty title - val mockPackageManager = mock(PackageManager::class.java) + val mockPackageManager = mock<PackageManager>() context.setMockPackageManager(mockPackageManager) whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) whenever(controller.metadata) @@ -626,7 +627,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { fun testOnNotificationAdded_blankTitle_hasPlaceholder() { // GIVEN that the manager has a notification with a blank title, and the app is not // required to include a non-empty title - val mockPackageManager = mock(PackageManager::class.java) + val mockPackageManager = mock<PackageManager>() context.setMockPackageManager(mockPackageManager) whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) whenever(controller.metadata) @@ -656,7 +657,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { fun testOnNotificationAdded_emptyMetadata_usesNotificationTitle() { // When the app sets the metadata title fields to empty strings, but does include a // non-blank notification title - val mockPackageManager = mock(PackageManager::class.java) + val mockPackageManager = mock<PackageManager>() context.setMockPackageManager(mockPackageManager) whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) whenever(controller.metadata) @@ -1610,6 +1611,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testTooManyCompactActions_isTruncated() { // GIVEN a notification where too many compact actions were specified @@ -1646,6 +1648,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .isEqualTo(MediaDataProcessor.MAX_COMPACT_ACTIONS) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testTooManyNotificationActions_isTruncated() { // GIVEN a notification where too many notification actions are added @@ -1681,6 +1684,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .isEqualTo(MediaDataProcessor.MAX_NOTIFICATION_ACTIONS) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_noState_usesNotification() { val desc = "Notification Action" @@ -1714,6 +1718,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_hasPrevNext() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") @@ -1757,6 +1762,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1]) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_noPrevNext_usesCustom() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") @@ -1789,6 +1795,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3]) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_connecting() { val stateActions = PlaybackState.ACTION_PLAY @@ -1874,6 +1881,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .isNotEqualTo(firstSemanticActions.prevOrCustom?.icon) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_reservedSpace() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") @@ -1912,6 +1920,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(actions.reservePrev).isTrue() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackActions_playPause_hasButton() { val stateActions = PlaybackState.ACTION_PLAY_PAUSE @@ -2074,6 +2083,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(mediaDataCaptor.value.semanticActions).isNotNull() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testPlaybackStateNull_Pause_keyExists_callsListener() { whenever(controller.playbackState).thenReturn(null) @@ -2132,6 +2142,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(mediaDataCaptor.value.isClearable).isFalse() } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_notifRemoved_setToResume() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) @@ -2162,6 +2173,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { ) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) @@ -2180,6 +2192,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3) @Test fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() { fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt index 8a6df1cbb4de..d88d69da5e59 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt @@ -63,6 +63,7 @@ class DragAndDropTest : SysuiTestCase() { listState = listState, otherTiles = listOf(), columns = 4, + largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), onRemoveTile = {}, onSetTiles = onSetTiles, @@ -75,7 +76,7 @@ class DragAndDropTest : SysuiTestCase() { @Test fun draggedTile_shouldDisappear() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) } @@ -101,7 +102,7 @@ class DragAndDropTest : SysuiTestCase() { @Test fun draggedTile_shouldChangePosition() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) } @@ -128,7 +129,7 @@ class DragAndDropTest : SysuiTestCase() { @Test fun draggedTileOut_shouldBeRemoved() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) } @@ -153,7 +154,7 @@ class DragAndDropTest : SysuiTestCase() { @Test fun draggedNewTileIn_shouldBeAdded() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index d9c1d998798c..fac5ecb49027 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt @@ -62,6 +62,7 @@ class ResizingTest : SysuiTestCase() { listState = listState, otherTiles = listOf(), columns = 4, + largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), onRemoveTile = {}, onSetTiles = {}, @@ -74,7 +75,7 @@ class ResizingTest : SysuiTestCase() { @Test fun toggleIconTile_shouldBeLarge() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } @@ -90,7 +91,7 @@ class ResizingTest : SysuiTestCase() { @Test fun toggleLargeTile_shouldBeIcon() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } @@ -106,7 +107,7 @@ class ResizingTest : SysuiTestCase() { @Test fun resizedLarge_shouldBeIcon() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } @@ -126,7 +127,7 @@ class ResizingTest : SysuiTestCase() { @Test fun resizedIcon_shouldBeLarge() { var tiles by mutableStateOf(TestEditTiles) - val listState = EditTileListState(tiles, 4) + val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) composeRule.setContent { EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 4b648a3e76e7..d1e4f646a382 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.phone.fragment; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS; -import static com.android.systemui.Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN; @@ -61,6 +60,7 @@ import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.OperatorNameViewController; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; +import com.android.systemui.statusbar.core.StatusBarRootModernization; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder; @@ -156,7 +156,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testDisableNone() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -167,7 +167,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testDisableSystemInfo_systemAnimationIdle_doesHide() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -185,7 +185,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testSystemStatusAnimation_startedDisabled_finishedWithAnimator_showsSystemInfo() { // GIVEN the status bar hides the system info via disable flags, while there is no event CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -215,7 +215,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testSystemStatusAnimation_systemInfoDisabled_staysInvisible() { // GIVEN the status bar hides the system info via disable flags, while there is no event CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -232,7 +232,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testSystemStatusAnimation_notDisabled_animatesAlphaZero() { // GIVEN the status bar is not disabled CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -248,7 +248,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testSystemStatusAnimation_notDisabled_animatesBackToAlphaOne() { // GIVEN the status bar is not disabled CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -272,7 +272,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testDisableNotifications() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -290,7 +290,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @EnableFlags(StatusBarRootModernization.FLAG_NAME) public void testDisableNotifications_doesNothingWhenFlagEnabled() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -308,7 +308,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testDisableClock() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -326,7 +326,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @EnableFlags(StatusBarRootModernization.FLAG_NAME) public void testDisableClock_doesNothingWhenFlagEnabled() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -345,7 +345,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @DisableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_shadeOpenAndShouldHide_everythingHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -363,7 +363,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @DisableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_shadeOpenButNotShouldHide_everythingShown() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -382,7 +382,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { /** Regression test for b/279790651. */ @Test @DisableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_shadeOpenAndShouldHide_thenShadeNotOpenAndDozingUpdate_everythingShown() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -410,7 +410,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_notTransitioningToOccluded_everythingShown() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -426,7 +426,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @DisableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_isTransitioningToOccluded_everythingHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -442,7 +442,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @DisableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_wasTransitioningToOccluded_transitionFinished_everythingShown() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -473,7 +473,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME}) public void disable_noOngoingCall_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -485,7 +485,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME}) public void disable_hasOngoingCall_chipDisplayedAndNotificationIconsHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -498,7 +498,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME}) public void disable_hasOngoingCallButNotificationIconsDisabled_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -511,7 +511,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME}) public void disable_hasOngoingCallButAlsoHun_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -524,7 +524,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME}) public void disable_ongoingCallEnded_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -548,7 +548,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME}) public void disable_hasOngoingCall_hidesNotifsWithoutAnimation() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); // Enable animations for testing so that we can verify we still aren't animating @@ -565,7 +565,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME}) public void screenSharingChipsDisabled_ignoresNewCallback() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -599,7 +599,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void noOngoingActivity_chipHidden() { resumeAndGetFragment(); @@ -617,7 +617,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void hasPrimaryOngoingActivity_primaryChipDisplayedAndNotificationIconsHidden() { resumeAndGetFragment(); @@ -634,8 +634,8 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @EnableFlags({ FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME, - FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) - public void hasPrimaryOngoingActivity_viewsUnchangedWhenSimpleFragmentFlagOn() { + StatusBarRootModernization.FLAG_NAME}) + public void hasPrimaryOngoingActivity_viewsUnchangedWhenRootModernizationFlagOn() { resumeAndGetFragment(); assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility()); @@ -660,7 +660,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME}) public void hasSecondaryOngoingActivity_butNotifsFlagOff_secondaryChipHidden() { resumeAndGetFragment(); @@ -674,7 +674,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void hasSecondaryOngoingActivity_flagOn_secondaryChipShownAndNotificationIconsHidden() { resumeAndGetFragment(); @@ -689,7 +689,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME}) public void hasOngoingActivityButNotificationIconsDisabled_chipHidden_notifsFlagOff() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -706,7 +706,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_notifsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -724,7 +724,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME}) public void hasOngoingActivityButAlsoHun_chipHidden_notifsFlagOff() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -741,7 +741,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void hasOngoingActivitiesButAlsoHun_chipsHidden_notifsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -759,7 +759,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME}) public void primaryOngoingActivityEnded_chipHidden_notifsFlagOff() { resumeAndGetFragment(); @@ -782,7 +782,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void primaryOngoingActivityEnded_chipHidden_notifsFlagOn() { resumeAndGetFragment(); @@ -805,7 +805,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void secondaryOngoingActivityEnded_chipHidden() { resumeAndGetFragment(); @@ -828,7 +828,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME}) public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOff() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); // Enable animations for testing so that we can verify we still aren't animating @@ -847,7 +847,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); // Enable animations for testing so that we can verify we still aren't animating @@ -866,7 +866,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT}) + @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME}) public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOff() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -899,7 +899,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -933,7 +933,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void isHomeStatusBarAllowedByScene_false_everythingHidden() { resumeAndGetFragment(); @@ -947,7 +947,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void isHomeStatusBarAllowedByScene_true_everythingShown() { resumeAndGetFragment(); @@ -961,7 +961,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_isHomeStatusBarAllowedBySceneFalse_disableValuesIgnored() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -979,7 +979,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_isHomeStatusBarAllowedBySceneTrue_disableValuesUsed() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -997,7 +997,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @DisableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void isHomeStatusBarAllowedByScene_sceneContainerDisabled_valueNotUsed() { resumeAndGetFragment(); @@ -1011,7 +1011,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_isDozing_clockAndSystemInfoVisible() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mStatusBarStateController.isDozing()).thenReturn(true); @@ -1023,7 +1023,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_NotDozing_clockAndSystemInfoVisible() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mStatusBarStateController.isDozing()).thenReturn(false); @@ -1035,7 +1035,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_headsUpShouldBeVisibleTrue_clockDisabled() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true); @@ -1046,7 +1046,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void disable_headsUpShouldBeVisibleFalse_clockNotDisabled() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(false); @@ -1100,7 +1100,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @DisableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testStatusBarIcons_hiddenThroughoutCameraLaunch() { final CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -1123,7 +1123,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @DisableSceneContainer - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testStatusBarIcons_hiddenThroughoutLockscreenToDreamTransition() { final CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -1159,7 +1159,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) + @DisableFlags(StatusBarRootModernization.FLAG_NAME) public void testStatusBarIcons_lockscreenToDreamTransitionButNotDreaming_iconsVisible() { final CollapsedStatusBarFragment fragment = resumeAndGetFragment(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt index c435d3d99680..37671e0bc175 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt @@ -21,9 +21,9 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT import com.android.systemui.SysuiTestCase import com.android.systemui.animation.AnimatorTestRule +import com.android.systemui.statusbar.core.StatusBarRootModernization import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -38,7 +38,7 @@ private const val INITIAL_ALPHA = 1f @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest -@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT) +@DisableFlags(StatusBarRootModernization.FLAG_NAME) class MultiSourceMinAlphaControllerTest : SysuiTestCase() { private val view = View(context) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index c48898aad087..2e0b7c69f092 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -67,12 +67,12 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.data.repository.userRepository import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock import com.android.wifitrackerlib.MergedCarrierEntry import com.android.wifitrackerlib.WifiEntry @@ -96,6 +96,8 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @@ -137,6 +139,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private val wifiPickerTrackerCallback = argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>() private val vcnTransportInfo = VcnTransportInfo.Builder().build() + private val userRepository = kosmos.fakeUserRepository private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -159,7 +162,14 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { logcatTableLogBuffer(kosmos, "test") } - whenever(wifiPickerTrackerFactory.create(any(), capture(wifiPickerTrackerCallback), any())) + whenever( + wifiPickerTrackerFactory.create( + any(), + any(), + capture(wifiPickerTrackerCallback), + any(), + ) + ) .thenReturn(wifiPickerTracker) // For convenience, set up the subscription info callbacks @@ -188,6 +198,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { wifiRepository = WifiRepositoryImpl( + mContext, + userRepository, testScope.backgroundScope, mainExecutor, testDispatcher, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index 44e1437b909e..d823bf57c824 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -16,13 +16,19 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod +import android.content.Context +import android.content.pm.UserInfo import android.net.wifi.ScanResult import android.net.wifi.WifiManager import android.net.wifi.WifiManager.UNKNOWN_SSID import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo +import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.testing.TestableLooper import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.LogBuffer @@ -33,12 +39,10 @@ import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRep import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.data.repository.userRepository import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor -import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.fakeSystemClock import com.android.wifitrackerlib.HotspotNetworkEntry import com.android.wifitrackerlib.HotspotNetworkEntry.DeviceType @@ -56,7 +60,12 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.capture +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever /** * Note: Most of these tests are duplicates of [WifiRepositoryImplTest] tests. @@ -67,8 +76,9 @@ import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) -class WifiRepositoryImplTest : SysuiTestCase() { +class WifiRepositoryImplTest() : SysuiTestCase() { private val kosmos = testKosmos() + private val userRepository = kosmos.fakeUserRepository // Using lazy means that the class will only be constructed once it's fetched. Because the // repository internally sets some values on construction, we need to set up some test @@ -76,6 +86,8 @@ class WifiRepositoryImplTest : SysuiTestCase() { // inside each test case without needing to manually recreate the repository. private val underTest: WifiRepositoryImpl by lazy { WifiRepositoryImpl( + mContext, + userRepository, testScope.backgroundScope, executor, dispatcher, @@ -101,7 +113,8 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { - whenever(wifiPickerTrackerFactory.create(any(), capture(callbackCaptor), any())) + userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) + whenever(wifiPickerTrackerFactory.create(any(), any(), capture(callbackCaptor), any())) .thenReturn(wifiPickerTracker) } @@ -1203,6 +1216,95 @@ class WifiRepositoryImplTest : SysuiTestCase() { assertThat(latest).isEmpty() } + // TODO(b/371586248): This test currently require currentUserContext to be public for testing, + // this needs to + // be updated to capture the argument instead so currentUserContext can be private. + @Test + @EnableFlags(FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT) + fun oneUserVerifyCreatingWifiPickerTracker_multiuserFlagEnabled() = + testScope.runTest { + val primaryUserMockContext = mock<Context>() + mContext.prepareCreateContextAsUser( + UserHandle.of(PRIMARY_USER_ID), + primaryUserMockContext, + ) + + userRepository.setSelectedUserInfo(PRIMARY_USER) + runCurrent() + val currentUserContext by collectLastValue(underTest.selectedUserContext) + + assertThat(currentUserContext).isEqualTo(primaryUserMockContext) + verify(wifiPickerTrackerFactory).create(any(), any(), any(), any()) + } + + // TODO(b/371586248): This test currently require currentUserContext to be public for testing, + // this needs to + // be updated to capture the argument instead so currentUserContext can be private. + @Test + @EnableFlags(FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT) + fun changeUserVerifyCreatingWifiPickerTracker_multiuserEnabled() = + testScope.runTest { + val primaryUserMockContext = mock<Context>() + mContext.prepareCreateContextAsUser( + UserHandle.of(PRIMARY_USER_ID), + primaryUserMockContext, + ) + + runCurrent() + userRepository.setSelectedUserInfo(PRIMARY_USER) + runCurrent() + val currentUserContext by collectLastValue(underTest.selectedUserContext) + + assertThat(currentUserContext).isEqualTo(primaryUserMockContext) + + val otherUserMockContext = mock<Context>() + mContext.prepareCreateContextAsUser( + UserHandle.of(ANOTHER_USER_ID), + otherUserMockContext, + ) + + runCurrent() + userRepository.setSelectedUserInfo(ANOTHER_USER) + runCurrent() + val otherUserContext by collectLastValue(underTest.selectedUserContext) + + assertThat(otherUserContext).isEqualTo(otherUserMockContext) + verify(wifiPickerTrackerFactory, times(2)).create(any(), any(), any(), any()) + } + + // TODO(b/371586248): This test currently require currentUserContext to be public for testing, + // this needs to + // be updated to capture the argument instead so currentUserContext can be private. + @Test + @DisableFlags(FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT) + fun changeUserVerifyCreatingWifiPickerTracker_multiuserDisabled() = + testScope.runTest { + val primaryUserMockContext = mock<Context>() + mContext.prepareCreateContextAsUser( + UserHandle.of(PRIMARY_USER_ID), + primaryUserMockContext, + ) + + runCurrent() + userRepository.setSelectedUserInfo(PRIMARY_USER) + runCurrent() + val currentUserContext by collectLastValue(underTest.selectedUserContext) + + assertThat(currentUserContext).isEqualTo(primaryUserMockContext) + + val otherUserMockContext = mock<Context>() + mContext.prepareCreateContextAsUser( + UserHandle.of(ANOTHER_USER_ID), + otherUserMockContext, + ) + + runCurrent() + userRepository.setSelectedUserInfo(ANOTHER_USER) + runCurrent() + + verify(wifiPickerTrackerFactory, times(1)).create(any(), any(), any(), any()) + } + private fun getCallback(): WifiPickerTracker.WifiPickerTrackerCallback { testScope.runCurrent() return callbackCaptor.value @@ -1231,5 +1333,20 @@ class WifiRepositoryImplTest : SysuiTestCase() { private companion object { const val TITLE = "AB" + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PROFILE, + ) + + private const val ANOTHER_USER_ID = 1 + private val ANOTHER_USER = + UserInfo( + /* id= */ ANOTHER_USER_ID, + /* name= */ "another user", + /* flags= */ UserInfo.FLAG_PROFILE, + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java index 38a61fecdc8a..21adeb01487b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java @@ -316,6 +316,31 @@ public class WalletScreenControllerTest extends SysuiTestCase { } @Test + public void queryCards_hasCards_showCarousel_invalidIconSource_noIcon() { + GetWalletCardsResponse response = + new GetWalletCardsResponse( + Collections.singletonList(createWalletCardWithInvalidIcon(mContext)), 0); + + mController.queryWalletCards(); + mTestableLooper.processAllMessages(); + + verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture()); + + QuickAccessWalletClient.OnWalletCardsRetrievedCallback callback = + mCallbackCaptor.getValue(); + + assertEquals(mController, callback); + + callback.onWalletCardsRetrieved(response); + mTestableLooper.processAllMessages(); + + assertEquals(VISIBLE, mWalletView.getCardCarousel().getVisibility()); + assertEquals(GONE, mWalletView.getEmptyStateView().getVisibility()); + assertEquals(GONE, mWalletView.getErrorView().getVisibility()); + assertEquals(null, mWalletView.getIcon().getDrawable()); + } + + @Test public void queryCards_noCards_showEmptyState() { GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.EMPTY_LIST, 0); @@ -507,6 +532,16 @@ public class WalletScreenControllerTest extends SysuiTestCase { .build(); } + private WalletCard createWalletCardWithInvalidIcon(Context context) { + PendingIntent pendingIntent = + PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE); + return new WalletCard.Builder( + CARD_ID_1, createIconWithInvalidSource(), "•••• 1234", pendingIntent) + .setCardIcon(createIconWithInvalidSource()) + .setCardLabel("Hold to reader") + .build(); + } + private WalletCard createCrazyWalletCard(Context context, boolean hasLabel) { PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE); @@ -520,6 +555,10 @@ public class WalletScreenControllerTest extends SysuiTestCase { return Icon.createWithBitmap(Bitmap.createBitmap(70, 44, Bitmap.Config.ARGB_8888)); } + private static Icon createIconWithInvalidSource() { + return Icon.createWithContentUri("content://media/external/images/media"); + } + private WalletCardViewInfo createCardViewInfo(WalletCard walletCard) { return new WalletScreenController.QAWalletCardViewInfo( mContext, walletCard); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 48106de5225b..fc318d56a8d5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -2395,7 +2395,8 @@ public class BubblesTest extends SysuiTestCase { FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); - mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT); + mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW); assertThat(bubbleStateListener.mLastUpdate).isNotNull(); assertThat(bubbleStateListener.mLastUpdate.bubbleBarLocation).isEqualTo( BubbleBarLocation.LEFT); @@ -2408,7 +2409,8 @@ public class BubblesTest extends SysuiTestCase { FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); - mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT); + mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW); assertThat(bubbleStateListener.mStateChangeCalls).isEqualTo(0); } @@ -2535,6 +2537,78 @@ public class BubblesTest extends SysuiTestCase { @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) @Test + public void testEventLogging_bubbleBar_dragBarLeft() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + mPositioner.setBubbleBarLocation(BubbleBarLocation.RIGHT); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + assertBarMode(); + + mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.DRAG_BAR); + + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BAR); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_dragBarRight() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + assertBarMode(); + + mBubbleController.setBubbleBarLocation(BubbleBarLocation.RIGHT, + BubbleBarLocation.UpdateSource.DRAG_BAR); + + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BAR); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_dragBubbleLeft() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + mPositioner.setBubbleBarLocation(BubbleBarLocation.RIGHT); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + assertBarMode(); + + mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.DRAG_BUBBLE); + + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BUBBLE); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_dragBubbleRight() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + assertBarMode(); + + mBubbleController.setBubbleBarLocation(BubbleBarLocation.RIGHT, + BubbleBarLocation.UpdateSource.DRAG_BUBBLE); + + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BUBBLE); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test public void testEventLogging_bubbleBar_expandAndCollapse() { mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt index 219794f3ad18..a7917a0866bb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt @@ -37,9 +37,7 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.currentTime -class FakeAuthenticationRepository( - private val currentTime: () -> Long, -) : AuthenticationRepository { +class FakeAuthenticationRepository(private val currentTime: () -> Long) : AuthenticationRepository { override val hintedPinLength: Int = HINTING_PIN_LENGTH @@ -72,6 +70,9 @@ class FakeAuthenticationRepository( private val credentialCheckingMutex = Mutex(locked = false) + var maximumTimeToLock: Long = 0 + var powerButtonInstantlyLocks: Boolean = true + override suspend fun getAuthenticationMethod(): AuthenticationMethodModel { return authenticationMethod.value } @@ -114,6 +115,7 @@ class FakeAuthenticationRepository( MAX_FAILED_AUTH_TRIES_BEFORE_WIPE var profileWithMinFailedUnlockAttemptsForWipe: Int = UserHandle.USER_SYSTEM + override suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int = profileWithMinFailedUnlockAttemptsForWipe @@ -144,10 +146,7 @@ class FakeAuthenticationRepository( val failedAttempts = _failedAuthenticationAttempts.value if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { - AuthenticationResultModel( - isSuccessful = isSuccessful, - lockoutDurationMs = 0, - ) + AuthenticationResultModel(isSuccessful = isSuccessful, lockoutDurationMs = 0) } else { AuthenticationResultModel( isSuccessful = false, @@ -178,6 +177,14 @@ class FakeAuthenticationRepository( credentialCheckingMutex.unlock() } + override suspend fun getMaximumTimeToLock(): Long { + return maximumTimeToLock + } + + override suspend fun getPowerButtonInstantlyLocks(): Boolean { + return powerButtonInstantlyLocks + } + private fun getExpectedCredential(securityMode: SecurityMode): List<Any> { return when (val credentialType = getCurrentCredentialType(securityMode)) { LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN @@ -219,9 +226,7 @@ class FakeAuthenticationRepository( } @LockPatternUtils.CredentialType - private fun getCurrentCredentialType( - securityMode: SecurityMode, - ): Int { + private fun getCurrentCredentialType(securityMode: SecurityMode): Int { return when (securityMode) { SecurityMode.PIN, SecurityMode.SimPin, @@ -260,9 +265,8 @@ class FakeAuthenticationRepository( object FakeAuthenticationRepositoryModule { @Provides @SysUISingleton - fun provideFake( - scope: TestScope, - ) = FakeAuthenticationRepository(currentTime = { scope.currentTime }) + fun provideFake(scope: TestScope) = + FakeAuthenticationRepository(currentTime = { scope.currentTime }) @Module interface Bindings { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt index 2dcd275f0103..f6ff4c4a057e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.deviceentry.data.repository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus import dagger.Binds import dagger.Module import javax.inject.Inject @@ -35,6 +36,9 @@ class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository { private var pendingLockscreenEnabled = _isLockscreenEnabled.value + override val deviceUnlockStatus = + MutableStateFlow(DeviceUnlockStatus(isUnlocked = false, deviceUnlockSource = null)) + override suspend fun isLockscreenEnabled(): Boolean { _isLockscreenEnabled.value = pendingLockscreenEnabled return isLockscreenEnabled.value diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt index 8922b2f5c5ef..e4c7df64fdc6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt @@ -19,25 +19,27 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository import com.android.systemui.flags.fakeSystemPropertiesHelper -import com.android.systemui.flags.systemPropertiesHelper -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.trustInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository val Kosmos.deviceUnlockedInteractor by Fixture { DeviceUnlockedInteractor( - applicationScope = applicationCoroutineScope, - authenticationInteractor = authenticationInteractor, - deviceEntryRepository = deviceEntryRepository, - trustInteractor = trustInteractor, - faceAuthInteractor = deviceEntryFaceAuthInteractor, - fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, - powerInteractor = powerInteractor, - biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, - systemPropertiesHelper = fakeSystemPropertiesHelper, - keyguardTransitionInteractor = keyguardTransitionInteractor, - ) + authenticationInteractor = authenticationInteractor, + repository = deviceEntryRepository, + trustInteractor = trustInteractor, + faceAuthInteractor = deviceEntryFaceAuthInteractor, + fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, + powerInteractor = powerInteractor, + biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, + systemPropertiesHelper = fakeSystemPropertiesHelper, + userAwareSecureSettingsRepository = userAwareSecureSettingsRepository, + keyguardInteractor = keyguardInteractor, + ) + .apply { activateIn(testScope) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index 19e077c57de0..8209ee12ad9a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -87,7 +87,7 @@ class FakeKeyguardTransitionRepository( ) : this( initInLockscreen = true, initiallySendTransitionStepsOnStartTransition = true, - testScope + testScope, ) private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> = @@ -191,12 +191,12 @@ class FakeKeyguardTransitionRepository( if (lastStep != null && lastStep.transitionState != TransitionState.FINISHED) { sendTransitionStep( step = - TransitionStep( - transitionState = TransitionState.CANCELED, - from = lastStep.from, - to = lastStep.to, - value = 0f, - ) + TransitionStep( + transitionState = TransitionState.CANCELED, + from = lastStep.from, + to = lastStep.to, + value = 0f, + ) ) testScheduler.runCurrent() } @@ -390,6 +390,18 @@ class FakeKeyguardTransitionRepository( @FloatRange(from = 0.0, to = 1.0) value: Float, state: TransitionState, ) = Unit + + override suspend fun forceFinishCurrentTransition() { + _transitions.tryEmit( + TransitionStep( + _currentTransitionInfo.value.from, + _currentTransitionInfo.value.to, + 1f, + TransitionState.FINISHED, + ownerName = _currentTransitionInfo.value.ownerName, + ) + ) + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt index aa94c368e8f1..b9a831f11d23 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by @@ -26,6 +27,7 @@ val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by KeyguardTransitionInteractor( scope = applicationCoroutineScope, repository = keyguardTransitionRepository, - sceneInteractor = sceneInteractor + sceneInteractor = sceneInteractor, + powerInteractor = powerInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt index f49e3771763a..b3be2c09c6f8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt @@ -22,6 +22,7 @@ import android.os.Handler import android.os.looper import androidx.media3.session.CommandButton import androidx.media3.session.MediaController +import androidx.media3.session.SessionCommand import androidx.media3.session.SessionToken import com.android.systemui.Flags import com.android.systemui.graphics.imageLoader @@ -30,7 +31,11 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.shared.mediaLogger import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.fakeSessionTokenFactory +import com.android.systemui.util.concurrency.execution import com.google.common.collect.ImmutableList +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -46,10 +51,22 @@ var Kosmos.media3ActionFactory: Media3ActionFactory by mock<MediaController>().also { whenever(it.customLayout).thenReturn(customLayout) whenever(it.sessionExtras).thenReturn(Bundle()) + whenever(it.isCommandAvailable(any())).thenReturn(true) + whenever(it.isSessionCommandAvailable(any<SessionCommand>())).thenReturn(true) } fakeMediaControllerFactory.setMedia3Controller(media3Controller) fakeSessionTokenFactory.setMedia3SessionToken(mock<SessionToken>()) } + + val runnableCaptor = argumentCaptor<Runnable>() + val handler = + mock<Handler> { + on { post(runnableCaptor.capture()) } doAnswer + { + runnableCaptor.lastValue.run() + true + } + } Media3ActionFactory( context = applicationContext, imageLoader = imageLoader, @@ -57,7 +74,8 @@ var Kosmos.media3ActionFactory: Media3ActionFactory by tokenFactory = fakeSessionTokenFactory, logger = mediaLogger, looper = looper, - handler = Handler(looper), + handler = handler, bgScope = testScope, + execution = execution, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt new file mode 100644 index 000000000000..a977121b3803 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.data.repository + +import android.content.res.mainResources +import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope + +val Kosmos.largeTileSpanRepository by + Kosmos.Fixture { + LargeTileSpanRepository(applicationCoroutineScope, mainResources, configurationRepository) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryKosmos.kt index 513d4e418a41..1395b1818b30 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryKosmos.kt @@ -16,8 +16,10 @@ package com.android.systemui.qs.panels.data.repository +import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.settings.userFileManager import com.android.systemui.user.data.repository.userRepository @@ -27,6 +29,8 @@ val Kosmos.qsPreferencesRepository by userFileManager, userRepository, defaultLargeTilesRepository, - testDispatcher + testDispatcher, + FakeLogBuffer.Factory.create(), + broadcastDispatcher, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt index 0c62d0e85ce1..8d4db8b74061 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository +import com.android.systemui.qs.panels.data.repository.largeTileSpanRepository import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor val Kosmos.iconTilesInteractor by @@ -28,7 +29,8 @@ val Kosmos.iconTilesInteractor by defaultLargeTilesRepository, currentTilesInteractor, qsPreferencesInteractor, + largeTileSpanRepository, FakeLogBuffer.Factory.create(), - applicationCoroutineScope + applicationCoroutineScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt index 45aab860cde7..28edae7c3689 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt @@ -49,6 +49,7 @@ val Kosmos.statusBarOrchestrator by fakeStatusBarModePerDisplayRepository, fakeStatusBarInitializer, fakeStatusBarWindowController, + applicationCoroutineScope.coroutineContext, mockDemoModeController, mockPluginDependencyProvider, mockAutoHideController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index c3c3cce5cf68..dae66d42b2bc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -41,6 +41,7 @@ class FakeMobileConnectionRepository( override val isGsm = MutableStateFlow(false) override val cdmaLevel = MutableStateFlow(0) override val primaryLevel = MutableStateFlow(0) + override val satelliteLevel = MutableStateFlow(0) override val dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected) override val dataActivityDirection = MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 1808a5f99f4e..85d582a27faf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -72,6 +72,10 @@ class FakeUserRepository @Inject constructor() : UserRepository { override val isSecondaryUserLogoutEnabled: StateFlow<Boolean> = _isSecondaryUserLogoutEnabled.asStateFlow() + private val _isLogoutToSystemUserEnabled = MutableStateFlow<Boolean>(false) + override val isLogoutToSystemUserEnabled: StateFlow<Boolean> = + _isLogoutToSystemUserEnabled.asStateFlow() + override var mainUserId: Int = MAIN_USER_ID override var lastSelectedNonGuestUserId: Int = mainUserId @@ -123,6 +127,17 @@ class FakeUserRepository @Inject constructor() : UserRepository { logOutSecondaryUserCallCount++ } + fun setLogoutToSystemUserEnabled(logoutEnabled: Boolean) { + _isLogoutToSystemUserEnabled.value = logoutEnabled + } + + var logOutToSystemUserCallCount: Int = 0 + private set + + override suspend fun logOutToSystemUser() { + logOutToSystemUserCallCount++ + } + fun setUserInfos(infos: List<UserInfo>) { _userInfos.value = infos } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/FakeVolumeDialogRingerFeedbackRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/FakeVolumeDialogRingerFeedbackRepository.kt new file mode 100644 index 000000000000..d42de1e6d0df --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/FakeVolumeDialogRingerFeedbackRepository.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.ringer.data.repository + +class FakeVolumeDialogRingerFeedbackRepository : VolumeDialogRingerFeedbackRepository { + + private var seenToastCount = 0 + + override suspend fun getToastCount(): Int { + return seenToastCount + } + + override suspend fun updateToastCount(toastCount: Int) { + seenToastCount = toastCount + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt new file mode 100644 index 000000000000..44371b4615df --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.ringer.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeVolumeDialogRingerFeedbackRepository by + Kosmos.Fixture { FakeVolumeDialogRingerFeedbackRepository() } +val Kosmos.volumeDialogRingerFeedbackRepository by + Kosmos.Fixture { fakeVolumeDialogRingerFeedbackRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt index 1addd91d2ec2..a494d04ec741 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController import com.android.systemui.volume.data.repository.audioSystemRepository import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor +import com.android.systemui.volume.dialog.ringer.data.repository.fakeVolumeDialogRingerFeedbackRepository val Kosmos.volumeDialogRingerInteractor by Kosmos.Fixture { @@ -29,5 +30,6 @@ val Kosmos.volumeDialogRingerInteractor by volumeDialogStateInteractor = volumeDialogStateInteractor, controller = volumeDialogController, audioSystemRepository = audioSystemRepository, + ringerFeedbackRepository = fakeVolumeDialogRingerFeedbackRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt index db1c01a8698c..c8ba551c518a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt @@ -16,20 +16,24 @@ package com.android.systemui.volume.dialog.ringer.ui.viewmodel +import android.content.applicationContext import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor import com.android.systemui.volume.dialog.ringer.domain.volumeDialogRingerInteractor import com.android.systemui.volume.dialog.shared.volumeDialogLogger val Kosmos.volumeDialogRingerDrawerViewModel by Kosmos.Fixture { VolumeDialogRingerDrawerViewModel( + applicationContext = applicationContext, backgroundDispatcher = testDispatcher, coroutineScope = applicationCoroutineScope, interactor = volumeDialogRingerInteractor, vibrator = vibratorHelper, volumeDialogLogger = volumeDialogLogger, + visibilityInteractor = volumeDialogVisibilityInteractor, ) } diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt index 82be2c0db24e..c196a09e18d1 100644 --- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt @@ -15,7 +15,9 @@ com.android.internal.os.BatteryStatsHistory com.android.internal.os.BatteryStatsHistoryIterator com.android.internal.os.Clock com.android.internal.os.LongArrayMultiStateCounter +com.android.internal.os.LongArrayMultiStateCounter_ravenwood com.android.internal.os.LongMultiStateCounter +com.android.internal.os.LongMultiStateCounter_ravenwood com.android.internal.os.MonotonicClock com.android.internal.os.PowerProfile com.android.internal.os.PowerStats @@ -143,6 +145,7 @@ android.os.LocaleList android.os.Looper android.os.Message android.os.MessageQueue +android.os.MessageQueue_ravenwood android.os.PackageTagsList android.os.Parcel android.os.ParcelFileDescriptor @@ -235,6 +238,7 @@ android.database.Cursor android.database.CursorIndexOutOfBoundsException android.database.CursorJoiner android.database.CursorWindow +android.database.CursorWindow_ravenwood android.database.CursorWrapper android.database.DataSetObservable android.database.DataSetObserver @@ -370,4 +374,3 @@ android.app.compat.* com.android.server.compat.* com.android.internal.compat.* android.app.AppCompatCallbacks - diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index 617cca9d3075..d6fc6e461edc 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -27,6 +27,8 @@ import android.annotation.NonNull; import android.content.Context; import android.graphics.Region; import android.hardware.input.InputManager; +import android.hardware.input.KeyGestureEvent; +import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; @@ -44,11 +46,14 @@ import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; import android.view.accessibility.AccessibilityEvent; +import androidx.annotation.Nullable; + import com.android.server.LocalServices; import com.android.server.accessibility.gestures.TouchExplorer; import com.android.server.accessibility.magnification.FullScreenMagnificationController; import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler; import com.android.server.accessibility.magnification.FullScreenMagnificationVibrationHelper; +import com.android.server.accessibility.magnification.MagnificationController; import com.android.server.accessibility.magnification.MagnificationGestureHandler; import com.android.server.accessibility.magnification.MouseEventHandler; import com.android.server.accessibility.magnification.WindowMagnificationGestureHandler; @@ -187,6 +192,8 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo private final AccessibilityManagerService mAms; + private final InputManager mInputManager; + private final SparseArray<EventStreamTransformation> mEventHandler; private final SparseArray<TouchExplorer> mTouchExplorer = new SparseArray<>(0); @@ -228,6 +235,47 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo */ private MotionEvent mLastActiveDeviceMotionEvent = null; + private boolean mKeyGestureEventHandlerInstalled = false; + private InputManager.KeyGestureEventHandler mKeyGestureEventHandler = + new InputManager.KeyGestureEventHandler() { + @Override + public boolean handleKeyGestureEvent( + @NonNull KeyGestureEvent event, + @Nullable IBinder focusedToken) { + final boolean complete = + event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE + && !event.isCancelled(); + final int gestureType = event.getKeyGestureType(); + final int displayId = isDisplayIdValid(event.getDisplayId()) + ? event.getDisplayId() : Display.DEFAULT_DISPLAY; + + switch (gestureType) { + case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN: + if (complete) { + mAms.getMagnificationController().scaleMagnificationByStep( + displayId, MagnificationController.ZOOM_DIRECTION_IN); + } + return true; + case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT: + if (complete) { + mAms.getMagnificationController().scaleMagnificationByStep( + displayId, MagnificationController.ZOOM_DIRECTION_OUT); + } + return true; + } + return false; + } + + @Override + public boolean isKeyGestureSupported(int gestureType) { + return switch (gestureType) { + case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN, + KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT -> true; + default -> false; + }; + } + }; + private static MotionEvent cancelMotion(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT @@ -287,6 +335,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo mContext = context; mAms = service; mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mInputManager = context.getSystemService(InputManager.class); mEventHandler = eventHandler; } @@ -723,6 +772,12 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo createMagnificationGestureHandler(displayId, displayContext); addFirstEventHandler(displayId, magnificationGestureHandler); mMagnificationGestureHandler.put(displayId, magnificationGestureHandler); + + if (com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures() + && !mKeyGestureEventHandlerInstalled) { + mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler); + mKeyGestureEventHandlerInstalled = true; + } } if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) { @@ -842,6 +897,11 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo mMouseKeysInterceptor.onDestroy(); mMouseKeysInterceptor = null; } + + if (mKeyGestureEventHandlerInstalled) { + mInputManager.unregisterKeyGestureEventHandler(mKeyGestureEventHandler); + mKeyGestureEventHandlerInstalled = false; + } } private MagnificationGestureHandler createMagnificationGestureHandler( diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 974cba2450f4..d4af7b765254 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -42,9 +42,11 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATIN import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED; +import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import static android.view.accessibility.AccessibilityManager.FlashNotificationReason; +import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; import static com.android.hardware.input.Flags.keyboardA11yMouseKeys; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; @@ -55,6 +57,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; @@ -111,6 +114,8 @@ import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManager; import android.hardware.fingerprint.IFingerprintService; +import android.hardware.input.InputManager; +import android.hardware.input.KeyGestureEvent; import android.media.AudioManagerInternal; import android.net.Uri; import android.os.Binder; @@ -338,6 +343,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private AlertDialog mEnableTouchExplorationDialog; + private final InputManager mInputManager; + private AccessibilityInputFilter mInputFilter; private boolean mHasInputFilter; @@ -503,6 +510,25 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + private InputManager.KeyGestureEventHandler mKeyGestureEventHandler = + new InputManager.KeyGestureEventHandler() { + @Override + public boolean handleKeyGestureEvent( + @NonNull KeyGestureEvent event, + @Nullable IBinder focusedToken) { + return AccessibilityManagerService.this.handleKeyGestureEvent(event); + } + + @Override + public boolean isKeyGestureSupported(int gestureType) { + return switch (gestureType) { + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true; + default -> false; + }; + } + }; + @VisibleForTesting AccessibilityManagerService( Context context, @@ -542,6 +568,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mUmi = LocalServices.getService(UserManagerInternal.class); // TODO(b/255426725): not used on tests mVisibleBgUserIds = null; + mInputManager = context.getSystemService(InputManager.class); init(); } @@ -583,6 +610,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mUiAutomationManager, this); mFlashNotificationsController = new FlashNotificationsController(mContext); mUmi = LocalServices.getService(UserManagerInternal.class); + mInputManager = context.getSystemService(InputManager.class); if (UserManager.isVisibleBackgroundUsersEnabled()) { mVisibleBgUserIds = new SparseBooleanArray(); @@ -599,6 +627,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub registerBroadcastReceivers(); new AccessibilityContentObserver(mMainHandler).register( mContext.getContentResolver()); + if (enableTalkbackAndMagnifierKeyGestures()) { + mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler); + } disableAccessibilityMenuToMigrateIfNeeded(); } @@ -640,6 +671,79 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return mIsAccessibilityButtonShown; } + @VisibleForTesting + boolean handleKeyGestureEvent(KeyGestureEvent event) { + final boolean complete = + event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE + && !event.isCancelled(); + final int gestureType = event.getKeyGestureType(); + if (!complete) { + return false; + } + + String targetName; + switch (gestureType) { + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION: + targetName = MAGNIFICATION_CONTROLLER_NAME; + break; + case KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK: + targetName = mContext.getString(R.string.config_defaultSelectToSpeakService); + if (targetName.isEmpty()) { + return false; + } + + final ComponentName targetServiceComponent = TextUtils.isEmpty(targetName) + ? null : ComponentName.unflattenFromString(targetName); + AccessibilityServiceInfo accessibilityServiceInfo; + synchronized (mLock) { + AccessibilityUserState userState = getCurrentUserStateLocked(); + accessibilityServiceInfo = + userState.getInstalledServiceInfoLocked(targetServiceComponent); + } + if (accessibilityServiceInfo == null) { + return false; + } + + // Skip enabling if a warning dialog is required for the feature. + // TODO(b/377752960): Explore better options to instead show the warning dialog + // in this scenario. + if (isAccessibilityServiceWarningRequired(accessibilityServiceInfo)) { + Slog.w(LOG_TAG, + "Accessibility warning is required before this service can be " + + "activated automatically via KEY_GESTURE shortcut."); + return false; + } + break; + default: + return false; + } + + List<String> shortcutTargets = getAccessibilityShortcutTargets( + KEY_GESTURE); + if (!shortcutTargets.contains(targetName)) { + int userId; + synchronized (mLock) { + userId = mCurrentUserId; + } + // TODO(b/377752960): Add dialog to confirm enabling the service and to + // activate the first time. + enableShortcutForTargets(true, UserShortcutType.KEY_GESTURE, + List.of(targetName), userId); + + // Do not perform action on first press since it was just registered. Eventually, + // this will be a separate dialog that appears that requires the user to confirm + // which will resolve this race condition. For now, just require two presses the + // first time it is activated. + return true; + } + + final int displayId = event.getDisplayId() != INVALID_DISPLAY + ? event.getDisplayId() : getLastNonProxyTopFocusedDisplayId(); + performAccessibilityShortcutInternal(displayId, KEY_GESTURE, targetName); + + return true; + } + @Override public Pair<float[], MagnificationSpec> getWindowTransformationMatrixAndMagnificationSpec( int windowId) { @@ -1224,14 +1328,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub int displayId = event.getDisplayId(); final int windowId = event.getWindowId(); if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID - && displayId == Display.INVALID_DISPLAY) { + && displayId == INVALID_DISPLAY) { displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId( resolvedUserId, windowId); event.setDisplayId(displayId); } synchronized (mLock) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED - && displayId != Display.INVALID_DISPLAY + && displayId != INVALID_DISPLAY && mA11yWindowManager.isTrackingWindowsLocked(displayId)) { shouldComputeWindows = true; } @@ -3257,6 +3361,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub updateAccessibilityShortcutTargetsLocked(userState, SOFTWARE); updateAccessibilityShortcutTargetsLocked(userState, GESTURE); updateAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS); + updateAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE); // Update the capabilities before the mode because we will check the current mode is // invalid or not.. updateMagnificationCapabilitiesSettingsChangeLocked(userState); @@ -3387,6 +3492,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS); somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, SOFTWARE); somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, GESTURE); + somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE); somethingChanged |= readAccessibilityButtonTargetComponentLocked(userState); somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState); somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState); @@ -3968,6 +4074,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (android.provider.Flags.a11yStandaloneGestureEnabled()) { shortcutTypes.add(GESTURE); } + shortcutTypes.add(KEY_GESTURE); final ComponentName serviceName = service.getComponentName(); for (Integer shortcutType: shortcutTypes) { @@ -4078,13 +4185,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub */ private void performAccessibilityShortcutInternal(int displayId, @UserShortcutType int shortcutType, @Nullable String targetName) { - final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(shortcutType); + final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal( + shortcutType); if (shortcutTargets.isEmpty()) { Slog.d(LOG_TAG, "No target to perform shortcut, shortcutType=" + shortcutType); return; } // In case the caller specified a target name - if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets, targetName)) { + if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets, + targetName)) { Slog.v(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName); targetName = null; } @@ -4306,6 +4415,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return; } + if (shortcutType == UserShortcutType.KEY_GESTURE + && !enableTalkbackAndMagnifierKeyGestures()) { + Slog.w(LOG_TAG, + "KEY_GESTURE type shortcuts are disabled by feature flag"); + return; + } + final String shortcutTypeSettingKey = ShortcutUtils.convertToKey(shortcutType); if (shortcutType == UserShortcutType.TRIPLETAP || shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP) { @@ -5071,6 +5187,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @EnforcePermission(MANAGE_ACCESSIBILITY) public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) { isAccessibilityServiceWarningRequired_enforcePermission(); + if (info == null) { + Log.e(LOG_TAG, "Called isAccessibilityServiceWarningRequired with null service info"); + return true; + } + final ComponentName componentName = info.getComponentName(); // Warning is not required if the service is already enabled. @@ -5678,6 +5799,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final Uri mAccessibilityGestureTargetsUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS); + private final Uri mAccessibilityKeyGestureTargetsUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS); + private final Uri mUserNonInteractiveUiTimeoutUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS); @@ -5742,6 +5866,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub contentResolver.registerContentObserver( mAccessibilityGestureTargetsUri, false, this, UserHandle.USER_ALL); contentResolver.registerContentObserver( + mAccessibilityKeyGestureTargetsUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( mUserNonInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL); contentResolver.registerContentObserver( mUserInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL); @@ -5823,6 +5949,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (readAccessibilityShortcutTargetsLocked(userState, GESTURE)) { onUserStateChangedLocked(userState); } + } else if (mAccessibilityKeyGestureTargetsUri.equals(uri)) { + if (readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE)) { + onUserStateChangedLocked(userState); + } } else if (mUserNonInteractiveUiTimeoutUri.equals(uri) || mUserInteractiveUiTimeoutUri.equals(uri)) { readUserRecommendedUiTimeoutSettingsLocked(userState); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 67b40632dde8..8b3e63d0dc5e 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -29,6 +29,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; @@ -209,6 +210,7 @@ class AccessibilityUserState { mShortcutTargets.put(SOFTWARE, new ArraySet<>()); mShortcutTargets.put(GESTURE, new ArraySet<>()); mShortcutTargets.put(QUICK_SETTINGS, new ArraySet<>()); + mShortcutTargets.put(KEY_GESTURE, new ArraySet<>()); } boolean isHandlingAccessibilityEventsLocked() { diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index d40e7476f7ec..51c4305061f8 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -27,6 +27,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static com.android.server.accessibility.AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID; import android.accessibilityservice.MagnificationConfig; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -101,6 +102,7 @@ public class MagnificationController implements MagnificationConnectionManager.C private int mMagnificationCapabilities = ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; /** Whether the platform supports window magnification feature. */ private final boolean mSupportWindowMagnification; + private final MagnificationScaleStepProvider mScaleStepProvider; private final Executor mBackgroundExecutor; @@ -131,6 +133,14 @@ public class MagnificationController implements MagnificationConnectionManager.C .UiChangesForAccessibilityCallbacks> mAccessibilityCallbacksDelegateArray = new SparseArray<>(); + // Direction magnifier scale can be altered. + public static final int ZOOM_DIRECTION_IN = 0; + public static final int ZOOM_DIRECTION_OUT = 1; + + @IntDef({ZOOM_DIRECTION_IN, ZOOM_DIRECTION_OUT}) + public @interface ZoomDirection { + } + /** * A callback to inform the magnification transition result on the given display. */ @@ -144,6 +154,41 @@ public class MagnificationController implements MagnificationConnectionManager.C void onResult(int displayId, boolean success); } + + /** + * An interface to configure how much the magnification scale should be affected when moving in + * steps. + */ + public interface MagnificationScaleStepProvider { + /** + * Calculate the next value given which direction (in/out) to adjust the magnification + * scale. + * + * @param currentScale The current magnification scale value. + * @param direction Whether to zoom in or out. + * @return The next scale value. + */ + float nextScaleStep(float currentScale, @ZoomDirection int direction); + } + + public static class DefaultMagnificationScaleStepProvider implements + MagnificationScaleStepProvider { + // Factor of magnification scale. For example, when this value is 1.189, scale + // value will be changed x1.000, x1.189, x1.414, x1.681, x2.000, ... + // Note: this value is 2.0 ^ (1 / 4). + public static final float ZOOM_STEP_SCALE_FACTOR = 1.18920712f; + + @Override + public float nextScaleStep(float currentScale, @ZoomDirection int direction) { + final int stepDelta = direction == ZOOM_DIRECTION_IN ? 1 : -1; + final long scaleIndex = Math.round( + Math.log(currentScale) / Math.log(ZOOM_STEP_SCALE_FACTOR)); + final float nextScale = (float) Math.pow(ZOOM_STEP_SCALE_FACTOR, + scaleIndex + stepDelta); + return MagnificationScaleProvider.constrainScale(nextScale); + } + } + public MagnificationController(AccessibilityManagerService ams, Object lock, Context context, MagnificationScaleProvider scaleProvider, Executor backgroundExecutor) { @@ -156,6 +201,7 @@ public class MagnificationController implements MagnificationConnectionManager.C .getAccessibilityController().setUiChangesForAccessibilityCallbacks(this); mSupportWindowMagnification = context.getPackageManager().hasSystemFeature( FEATURE_WINDOW_MAGNIFICATION); + mScaleStepProvider = new DefaultMagnificationScaleStepProvider(); mAlwaysOnMagnificationFeatureFlag = new AlwaysOnMagnificationFeatureFlag(context); mAlwaysOnMagnificationFeatureFlag.addOnChangedListener( @@ -891,6 +937,37 @@ public class MagnificationController implements MagnificationConnectionManager.C return isActivated; } + /** + * Scales the magnifier on the given display one step in/out based on the zoomIn param. + * + * @param displayId The logical display id. + * @param direction Whether the scale should be zoomed in or out. + * @return {@code true} if the magnification scale was affected. + */ + public boolean scaleMagnificationByStep(int displayId, @ZoomDirection int direction) { + if (getFullScreenMagnificationController().isActivated(displayId)) { + final float magnificationScale = getFullScreenMagnificationController().getScale( + displayId); + final float nextMagnificationScale = mScaleStepProvider.nextScaleStep( + magnificationScale, direction); + getFullScreenMagnificationController().setScaleAndCenter(displayId, + nextMagnificationScale, + Float.NaN, Float.NaN, true, MAGNIFICATION_GESTURE_HANDLER_ID); + return nextMagnificationScale != magnificationScale; + } + + if (getMagnificationConnectionManager().isWindowMagnifierEnabled(displayId)) { + final float magnificationScale = getMagnificationConnectionManager().getScale( + displayId); + final float nextMagnificationScale = mScaleStepProvider.nextScaleStep( + magnificationScale, direction); + getMagnificationConnectionManager().setScale(displayId, nextMagnificationScale); + return nextMagnificationScale != magnificationScale; + } + + return false; + } + private final class DisableMagnificationCallback implements MagnificationAnimationCallback { private final TransitionCallBack mTransitionCallBack; diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 268e56487c4b..f13e22950e2d 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -29,7 +29,7 @@ import android.app.appfunctions.AppFunctionManagerHelper; import android.app.appfunctions.AppFunctionRuntimeMetadata; import android.app.appfunctions.AppFunctionStaticMetadataHelper; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; -import android.app.appfunctions.ExecuteAppFunctionResponse; +import android.app.appfunctions.AppFunctionException; import android.app.appfunctions.IAppFunctionEnabledCallback; import android.app.appfunctions.IAppFunctionManager; import android.app.appfunctions.IAppFunctionService; @@ -156,11 +156,10 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { mCallerValidator.verifyTargetUserHandle( requestInternal.getUserHandle(), validatedCallingPackage); } catch (SecurityException exception) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_DENIED, - exception.getMessage(), - /* extras= */ null)); + safeExecuteAppFunctionCallback.onError( + new AppFunctionException( + AppFunctionException.ERROR_DENIED, + exception.getMessage())); return null; } @@ -180,7 +179,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { safeExecuteAppFunctionCallback, executeAppFunctionCallback.asBinder()); } catch (Exception e) { - safeExecuteAppFunctionCallback.onResult( + safeExecuteAppFunctionCallback.onError( mapExceptionToExecuteAppFunctionResponse(e)); } }); @@ -198,22 +197,19 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { UserHandle targetUser = requestInternal.getUserHandle(); // TODO(b/354956319): Add and honor the new enterprise policies. if (mCallerValidator.isUserOrganizationManaged(targetUser)) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, + safeExecuteAppFunctionCallback.onError( + new AppFunctionException(AppFunctionException.ERROR_SYSTEM_ERROR, "Cannot run on a device with a device owner or from the managed" - + " profile.", - /* extras= */ null)); + + " profile.")); return; } String targetPackageName = requestInternal.getClientRequest().getTargetPackageName(); if (TextUtils.isEmpty(targetPackageName)) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT, - "Target package name cannot be empty.", - /* extras= */ null)); + safeExecuteAppFunctionCallback.onError( + new AppFunctionException( + AppFunctionException.ERROR_INVALID_ARGUMENT, + "Target package name cannot be empty.")); return; } @@ -253,11 +249,10 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { mInternalServiceHelper.resolveAppFunctionService( targetPackageName, targetUser); if (serviceIntent == null) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, - "Cannot find the target service.", - /* extras= */ null)); + safeExecuteAppFunctionCallback.onError( + new AppFunctionException( + AppFunctionException.ERROR_SYSTEM_ERROR, + "Cannot find the target service.")); return; } bindAppFunctionServiceUnchecked( @@ -272,7 +267,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { }) .exceptionally( ex -> { - safeExecuteAppFunctionCallback.onResult( + safeExecuteAppFunctionCallback.onError( mapExceptionToExecuteAppFunctionResponse(ex)); return null; }); @@ -446,11 +441,9 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { if (!bindServiceResult) { Slog.e(TAG, "Failed to bind to the AppFunctionService"); - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, - "Failed to bind the AppFunctionService.", - /* extras= */ null)); + safeExecuteAppFunctionCallback.onError( + new AppFunctionException(AppFunctionException.ERROR_SYSTEM_ERROR, + "Failed to bind the AppFunctionService.")); } } @@ -459,22 +452,21 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { .getSystemService(AppSearchManager.class); } - private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) { + private AppFunctionException mapExceptionToExecuteAppFunctionResponse(Throwable e) { if (e instanceof CompletionException) { e = e.getCause(); } - int resultCode = ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR; + int resultCode = AppFunctionException.ERROR_SYSTEM_ERROR; if (e instanceof AppSearchException appSearchException) { resultCode = mapAppSearchResultFailureCodeToExecuteAppFunctionResponse( appSearchException.getResultCode()); } else if (e instanceof SecurityException) { - resultCode = ExecuteAppFunctionResponse.RESULT_DENIED; + resultCode = AppFunctionException.ERROR_DENIED; } else if (e instanceof DisabledAppFunctionException) { - resultCode = ExecuteAppFunctionResponse.RESULT_DISABLED; + resultCode = AppFunctionException.ERROR_DISABLED; } - return ExecuteAppFunctionResponse.newFailure( - resultCode, e.getMessage(), /* extras= */ null); + return new AppFunctionException(resultCode, e.getMessage()); } private int mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(int resultCode) { @@ -485,13 +477,13 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { switch (resultCode) { case AppSearchResult.RESULT_NOT_FOUND: - return ExecuteAppFunctionResponse.RESULT_FUNCTION_NOT_FOUND; + return AppFunctionException.ERROR_FUNCTION_NOT_FOUND; case AppSearchResult.RESULT_INVALID_ARGUMENT: case AppSearchResult.RESULT_INTERNAL_ERROR: case AppSearchResult.RESULT_SECURITY_ERROR: // fall-through } - return ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR; + return AppFunctionException.ERROR_SYSTEM_ERROR; } private void registerAppSearchObserver(@NonNull TargetUser user) { diff --git a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java index 129be65f3153..c689bb92f8f7 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java @@ -17,6 +17,7 @@ package com.android.server.appfunctions; import android.annotation.NonNull; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; +import android.app.appfunctions.AppFunctionException; import android.app.appfunctions.ExecuteAppFunctionResponse; import android.app.appfunctions.IAppFunctionService; import android.app.appfunctions.ICancellationCallback; @@ -57,17 +58,22 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp mCancellationCallback, new IExecuteAppFunctionCallback.Stub() { @Override - public void onResult(ExecuteAppFunctionResponse response) { + public void onSuccess(ExecuteAppFunctionResponse response) { mSafeExecuteAppFunctionCallback.onResult(response); serviceUsageCompleteListener.onCompleted(); } + + @Override + public void onError(AppFunctionException error) { + mSafeExecuteAppFunctionCallback.onError(error); + serviceUsageCompleteListener.onCompleted(); + } }); } catch (Exception e) { - mSafeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR, - e.getMessage(), - /* extras= */ null)); + mSafeExecuteAppFunctionCallback.onError( + new AppFunctionException( + AppFunctionException.ERROR_APP_UNKNOWN_ERROR, + e.getMessage())); serviceUsageCompleteListener.onCompleted(); } } @@ -75,11 +81,9 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp @Override public void onFailedToConnect() { Slog.e(TAG, "Failed to connect to service"); - mSafeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR, - "Failed to connect to AppFunctionService", - /* extras= */ null)); + mSafeExecuteAppFunctionCallback.onError( + new AppFunctionException(AppFunctionException.ERROR_APP_UNKNOWN_ERROR, + "Failed to connect to AppFunctionService")); } @Override diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index ddccb3731cc1..466d477992b3 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -281,6 +281,9 @@ public class UserBackupManagerService { private static final int SCHEDULE_FILE_VERSION = 1; public static final String SETTINGS_PACKAGE = "com.android.providers.settings"; + + public static final String TELEPHONY_PROVIDER_PACKAGE = "com.android.providers.telephony"; + public static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup"; // Pseudoname that we use for the Package Manager metadata "package". diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java index f24a3c1afc86..508b62cd83f0 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java +++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java @@ -21,6 +21,7 @@ import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL; import static com.android.server.backup.UserBackupManagerService.SETTINGS_PACKAGE; import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE; +import static com.android.server.backup.UserBackupManagerService.TELEPHONY_PROVIDER_PACKAGE; import static com.android.server.backup.UserBackupManagerService.WALLPAPER_PACKAGE; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; @@ -75,6 +76,12 @@ public class BackupEligibilityRules { systemPackagesAllowedForProfileUser, Sets.newArraySet(WALLPAPER_PACKAGE, SETTINGS_PACKAGE)); + static { + if (UserManager.isHeadlessSystemUserMode()) { + systemPackagesAllowedForNonSystemUsers.add(TELEPHONY_PROVIDER_PACKAGE); + } + } + private final PackageManager mPackageManager; private final PackageManagerInternal mPackageManagerInternal; private final int mUserId; diff --git a/services/core/Android.bp b/services/core/Android.bp index 3ccad16073a7..aea16b08df49 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -158,6 +158,7 @@ java_library_static { "android.hardware.gnss-V2-java", "android.hardware.vibrator-V3-java", "app-compat-annotations", + "art_exported_aconfig_flags_lib", "framework-tethering.stubs.module_lib", "keepanno-annotations", "service-art.stubs.system_server", @@ -237,6 +238,7 @@ java_library_static { "connectivity_flags_lib", "device_config_service_flags_java", "dreams_flags_lib", + "aconfig_flags_java", "aconfig_new_storage_flags_lib", "powerstats_flags_lib", "locksettings_flags_lib", diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 23cee9db2138..1588e0421675 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -53,6 +53,7 @@ import com.android.server.am.DropboxRateLimiter; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; @@ -400,9 +401,18 @@ public class BootReceiver extends BroadcastReceiver { Slog.w(TAG, "Tombstone too large to add to DropBox: " + tombstone.toPath()); return; } - // Read the proto tombstone file as bytes. - final byte[] tombstoneBytes = Files.readAllBytes(tombstone.toPath()); + // Read the proto tombstone file as bytes. + // Previously used Files.readAllBytes() which internally creates a ThreadLocal BufferCache + // via ChannelInputStream that isn't properly released. Switched to + // FileInputStream.transferTo() which avoids the NIO channels completely, + // preventing the memory leak while maintaining the same functionality. + final byte[] tombstoneBytes; + try (FileInputStream fis = new FileInputStream(tombstone); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + fis.transferTo(baos); + tombstoneBytes = baos.toByteArray(); + } final File tombstoneProtoWithHeaders = File.createTempFile( tombstone.getName(), ".tmp", TOMBSTONE_TMP_DIR); Files.setPosixFilePermissions( diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 72a9a2d6de26..fa228627c255 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -88,6 +88,7 @@ import android.telephony.emergency.EmergencyNumber; import android.telephony.ims.ImsCallSession; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.MediaQualityStatus; +import android.telephony.satellite.NtnSignalStrength; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -440,6 +441,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private boolean[] mCarrierRoamingNtnEligible = null; private List<IntArray> mCarrierRoamingNtnAvailableServices; + private NtnSignalStrength[] mCarrierRoamingNtnSignalStrength; // Local cache to check if Satellite Modem is enabled private AtomicBoolean mIsSatelliteEnabled; @@ -745,6 +747,12 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mSCBMDuration = copyOf(mSCBMDuration, mNumPhones); mCarrierRoamingNtnMode = copyOf(mCarrierRoamingNtnMode, mNumPhones); mCarrierRoamingNtnEligible = copyOf(mCarrierRoamingNtnEligible, mNumPhones); + if (mCarrierRoamingNtnSignalStrength != null) { + mCarrierRoamingNtnSignalStrength = copyOf( + mCarrierRoamingNtnSignalStrength, mNumPhones); + } else { + mCarrierRoamingNtnSignalStrength = new NtnSignalStrength[mNumPhones]; + } // ds -> ss switch. if (mNumPhones < oldNumPhones) { cutListToSize(mCellInfo, mNumPhones); @@ -807,6 +815,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCarrierRoamingNtnMode[i] = false; mCarrierRoamingNtnEligible[i] = false; mCarrierRoamingNtnAvailableServices.add(i, new IntArray()); + mCarrierRoamingNtnSignalStrength[i] = new NtnSignalStrength( + NtnSignalStrength.NTN_SIGNAL_STRENGTH_NONE); } } } @@ -883,6 +893,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCarrierRoamingNtnMode = new boolean[numPhones]; mCarrierRoamingNtnEligible = new boolean[numPhones]; mCarrierRoamingNtnAvailableServices = new ArrayList<>(); + mCarrierRoamingNtnSignalStrength = new NtnSignalStrength[numPhones]; mIsSatelliteEnabled = new AtomicBoolean(); mWasSatelliteEnabledNotified = new AtomicBoolean(); @@ -932,6 +943,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCarrierRoamingNtnMode[i] = false; mCarrierRoamingNtnEligible[i] = false; mCarrierRoamingNtnAvailableServices.add(i, new IntArray()); + mCarrierRoamingNtnSignalStrength[i] = new NtnSignalStrength( + NtnSignalStrength.NTN_SIGNAL_STRENGTH_NONE); } mAppOps = mContext.getSystemService(AppOpsManager.class); @@ -1565,6 +1578,15 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { remove(r.binder); } } + if (events.contains( + TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED)) { + try { + r.callback.onCarrierRoamingNtnSignalStrengthChanged( + mCarrierRoamingNtnSignalStrength[r.phoneId]); + } catch (RemoteException ex) { + remove(r.binder); + } + } } } } @@ -3803,6 +3825,44 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } + + /** + * Notify external listeners that carrier roaming non-terrestrial network + * signal strength changed. + * @param subId subscription ID. + * @param ntnSignalStrength non-terrestrial network signal strength. + */ + public void notifyCarrierRoamingNtnSignalStrengthChanged(int subId, + @NonNull NtnSignalStrength ntnSignalStrength) { + if (!checkNotifyPermission("notifyCarrierRoamingNtnSignalStrengthChanged")) { + log("nnotifyCarrierRoamingNtnSignalStrengthChanged: caller does not have required " + + "permissions."); + return; + } + + if (VDBG) { + log("notifyCarrierRoamingNtnSignalStrengthChanged: " + + "subId=" + subId + " ntnSignalStrength=" + ntnSignalStrength.getLevel()); + } + + synchronized (mRecords) { + int phoneId = getPhoneIdFromSubId(subId); + mCarrierRoamingNtnSignalStrength[phoneId] = ntnSignalStrength; + for (Record r : mRecords) { + if (r.matchTelephonyCallbackEvent( + TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED) + && idMatch(r, subId, phoneId)) { + try { + r.callback.onCarrierRoamingNtnSignalStrengthChanged(ntnSignalStrength); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } + } + } + handleRemoveListLocked(); + } + } + @NeverCompile // Avoid size overhead of debugging code. @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { @@ -3858,6 +3918,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { pw.println("mSCBMDuration=" + mSCBMDuration[i]); pw.println("mCarrierRoamingNtnMode=" + mCarrierRoamingNtnMode[i]); pw.println("mCarrierRoamingNtnEligible=" + mCarrierRoamingNtnEligible[i]); + pw.println("mCarrierRoamingNtnSignalStrength=" + + mCarrierRoamingNtnSignalStrength[i]); // We need to obfuscate package names, and primitive arrays' native toString is ugly Pair<List<String>, int[]> carrierPrivilegeState = mCarrierPrivilegeStates.get(i); diff --git a/services/core/java/com/android/server/adaptiveauth/OWNERS b/services/core/java/com/android/server/adaptiveauth/OWNERS deleted file mode 100644 index b18810564d88..000000000000 --- a/services/core/java/com/android/server/adaptiveauth/OWNERS +++ /dev/null @@ -1 +0,0 @@ -hainingc@google.com
\ No newline at end of file diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index aa9ac6c36784..2eb9f3cb600f 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -124,6 +124,7 @@ import com.android.server.LocalServices; import com.android.server.Watchdog; import com.android.server.net.BaseNetworkObserver; import com.android.server.pm.UserManagerInternal; +import com.android.server.power.feature.PowerManagerFlags; import com.android.server.power.optimization.Flags; import com.android.server.power.stats.BatteryExternalStatsWorker; import com.android.server.power.stats.BatteryStatsDumpHelperImpl; @@ -195,6 +196,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub private final BatteryStats.BatteryStatsDumpHelper mDumpHelper; private final PowerStatsUidResolver mPowerStatsUidResolver = new PowerStatsUidResolver(); private final PowerAttributor mPowerAttributor; + private final PowerManagerFlags mPowerManagerFlags = new PowerManagerFlags(); private volatile boolean mMonitorEnabled = true; private boolean mRailsStatsCollectionEnabled = true; @@ -617,6 +619,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub BatteryConsumer.POWER_COMPONENT_ANY, Flags.streamlinedMiscBatteryStats()); + mStats.setMoveWscLoggingToNotifierEnabled( + mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled()); + mWorker.systemServicesReady(); mStats.systemServicesReady(mContext); mCpuWakeupStats.systemServicesReady(); diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java index b0f880710eb6..8a128582c507 100644 --- a/services/core/java/com/android/server/am/BroadcastController.java +++ b/services/core/java/com/android/server/am/BroadcastController.java @@ -593,7 +593,7 @@ class BroadcastController { originalStickyCallingUid, BackgroundStartPrivileges.NONE, false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */, null /* filterExtrasForReceiver */, - broadcast.originalCallingAppProcessState); + broadcast.originalCallingAppProcessState, mService.mPlatformCompat); queue.enqueueBroadcastLocked(r); } } @@ -1631,7 +1631,7 @@ class BroadcastController { receivers, resultToApp, resultTo, resultCode, resultData, resultExtras, ordered, sticky, false, userId, backgroundStartPrivileges, timeoutExempt, filterExtrasForReceiver, - callerAppProcessState); + callerAppProcessState, mService.mPlatformCompat); broadcastSentEventRecord.setBroadcastRecord(r); if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r); diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index f908c67d7ec9..38df10a0bc8c 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -42,6 +42,8 @@ import android.app.AppOpsManager; import android.app.BackgroundStartPrivileges; import android.app.BroadcastOptions; import android.app.BroadcastOptions.DeliveryGroupPolicy; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.IIntentReceiver; import android.content.Intent; @@ -55,10 +57,12 @@ import android.os.UserHandle; import android.util.ArrayMap; import android.util.IntArray; import android.util.PrintWriterPrinter; +import android.util.SparseBooleanArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.compat.PlatformCompat; import dalvik.annotation.optimization.NeverCompile; @@ -77,6 +81,15 @@ import java.util.function.BiFunction; * An active intent broadcast. */ final class BroadcastRecord extends Binder { + /** + * Limit the scope of the priority values to the process level. This means that priority values + * will only influence the order of broadcast delivery within the same process. + */ + @ChangeId + @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) + @VisibleForTesting + static final long CHANGE_LIMIT_PRIORITY_SCOPE = 371307720L; + final @NonNull Intent intent; // the original intent that generated us final @Nullable ComponentName targetComp; // original component name set on the intent final @Nullable ProcessRecord callerApp; // process that sent this @@ -417,13 +430,13 @@ final class BroadcastRecord extends Binder { @NonNull BackgroundStartPrivileges backgroundStartPrivileges, boolean timeoutExempt, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, - int callerAppProcessState) { + int callerAppProcessState, PlatformCompat platformCompat) { this(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType, requiredPermissions, excludedPermissions, excludedPackages, appOp, options, receivers, resultToApp, resultTo, resultCode, resultData, resultExtras, serialized, sticky, initialSticky, userId, -1, backgroundStartPrivileges, timeoutExempt, - filterExtrasForReceiver, callerAppProcessState); + filterExtrasForReceiver, callerAppProcessState, platformCompat); } BroadcastRecord(BroadcastQueue _queue, @@ -439,7 +452,7 @@ final class BroadcastRecord extends Binder { @NonNull BackgroundStartPrivileges backgroundStartPrivileges, boolean timeoutExempt, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, - int callerAppProcessState) { + int callerAppProcessState, PlatformCompat platformCompat) { if (_intent == null) { throw new NullPointerException("Can't construct with a null intent"); } @@ -466,7 +479,8 @@ final class BroadcastRecord extends Binder { urgent = calculateUrgent(_intent, _options); deferUntilActive = calculateDeferUntilActive(_callingUid, _options, _resultTo, _serialized, urgent); - blockedUntilBeyondCount = calculateBlockedUntilBeyondCount(receivers, _serialized); + blockedUntilBeyondCount = calculateBlockedUntilBeyondCount( + receivers, _serialized, platformCompat); scheduledTime = new long[delivery.length]; terminalTime = new long[delivery.length]; resultToApp = _resultToApp; @@ -730,7 +744,8 @@ final class BroadcastRecord extends Binder { } /** - * Determine if the result of {@link #calculateBlockedUntilBeyondCount(List, boolean)} + * Determine if the result of + * {@link #calculateBlockedUntilBeyondCount(List, boolean, PlatformCompat)} * has prioritized tranches of receivers. */ @VisibleForTesting @@ -754,37 +769,121 @@ final class BroadcastRecord extends Binder { */ @VisibleForTesting static @NonNull int[] calculateBlockedUntilBeyondCount( - @NonNull List<Object> receivers, boolean ordered) { + @NonNull List<Object> receivers, boolean ordered, PlatformCompat platformCompat) { final int N = receivers.size(); final int[] blockedUntilBeyondCount = new int[N]; - int lastPriority = 0; - int lastPriorityIndex = 0; - for (int i = 0; i < N; i++) { - if (ordered) { - // When sending an ordered broadcast, we need to block this - // receiver until all previous receivers have terminated + if (ordered) { + // When sending an ordered broadcast, we need to block this + // receiver until all previous receivers have terminated + for (int i = 0; i < N; i++) { blockedUntilBeyondCount[i] = i; + } + } else { + if (Flags.limitPriorityScope()) { + final boolean[] changeEnabled = calculateChangeStateForReceivers( + receivers, CHANGE_LIMIT_PRIORITY_SCOPE, platformCompat); + + // Priority of the previous tranche + int lastTranchePriority = 0; + // Priority of the current tranche + int currentTranchePriority = 0; + // Index of the last receiver in the previous tranche + int lastTranchePriorityIndex = -1; + // Index of the last receiver with change disabled in the previous tranche + int lastTrancheChangeDisabledIndex = -1; + // Index of the last receiver with change disabled in the current tranche + int currentTrancheChangeDisabledIndex = -1; + + for (int i = 0; i < N; i++) { + final int thisPriority = getReceiverPriority(receivers.get(i)); + if (i == 0) { + currentTranchePriority = thisPriority; + if (!changeEnabled[i]) { + currentTrancheChangeDisabledIndex = i; + } + continue; + } + + // Check if a new priority tranche has started + if (thisPriority != currentTranchePriority) { + // Update tranche boundaries and reset the disabled index. + if (currentTrancheChangeDisabledIndex != -1) { + lastTrancheChangeDisabledIndex = currentTrancheChangeDisabledIndex; + } + lastTranchePriority = currentTranchePriority; + lastTranchePriorityIndex = i - 1; + currentTranchePriority = thisPriority; + currentTrancheChangeDisabledIndex = -1; + } + if (!changeEnabled[i]) { + currentTrancheChangeDisabledIndex = i; + + // Since the change is disabled, block the current receiver until the + // last receiver in the previous tranche. + blockedUntilBeyondCount[i] = lastTranchePriorityIndex + 1; + } else if (thisPriority != lastTranchePriority) { + // If the changeId was disabled for an earlier receiver and the current + // receiver has a different priority, block the current receiver + // until that earlier receiver. + if (lastTrancheChangeDisabledIndex != -1) { + blockedUntilBeyondCount[i] = lastTrancheChangeDisabledIndex + 1; + } + } + } + // If the entire list is in the same priority tranche or no receivers had + // changeId disabled, mark as -1 to indicate that none of them need to wait + if (N > 0 && (lastTranchePriorityIndex == -1 + || (lastTrancheChangeDisabledIndex == -1 + && currentTrancheChangeDisabledIndex == -1))) { + Arrays.fill(blockedUntilBeyondCount, -1); + } } else { // When sending a prioritized broadcast, we only need to wait // for the previous tranche of receivers to be terminated - final int thisPriority = getReceiverPriority(receivers.get(i)); - if ((i == 0) || (thisPriority != lastPriority)) { - lastPriority = thisPriority; - lastPriorityIndex = i; - blockedUntilBeyondCount[i] = i; - } else { - blockedUntilBeyondCount[i] = lastPriorityIndex; + int lastPriority = 0; + int lastPriorityIndex = 0; + for (int i = 0; i < N; i++) { + final int thisPriority = getReceiverPriority(receivers.get(i)); + if ((i == 0) || (thisPriority != lastPriority)) { + lastPriority = thisPriority; + lastPriorityIndex = i; + blockedUntilBeyondCount[i] = i; + } else { + blockedUntilBeyondCount[i] = lastPriorityIndex; + } + } + // If the entire list is in the same priority tranche, mark as -1 to + // indicate that none of them need to wait + if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) { + Arrays.fill(blockedUntilBeyondCount, -1); } } } - // If the entire list is in the same priority tranche, mark as -1 to - // indicate that none of them need to wait - if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) { - Arrays.fill(blockedUntilBeyondCount, -1); - } return blockedUntilBeyondCount; } + @VisibleForTesting + static @NonNull boolean[] calculateChangeStateForReceivers(@NonNull List<Object> receivers, + long changeId, PlatformCompat platformCompat) { + final SparseBooleanArray changeStateForUids = new SparseBooleanArray(); + final int count = receivers.size(); + final boolean[] changeStateForReceivers = new boolean[count]; + for (int i = 0; i < count; ++i) { + final int receiverUid = getReceiverUid(receivers.get(i)); + final boolean isChangeEnabled; + final int idx = changeStateForUids.indexOfKey(receiverUid); + if (idx >= 0) { + isChangeEnabled = changeStateForUids.valueAt(idx); + } else { + isChangeEnabled = platformCompat.isChangeEnabledByUidInternalNoLogging( + changeId, receiverUid); + changeStateForUids.put(receiverUid, isChangeEnabled); + } + changeStateForReceivers[i] = isChangeEnabled; + } + return changeStateForReceivers; + } + static int getReceiverUid(@NonNull Object receiver) { if (receiver instanceof BroadcastFilter) { return ((BroadcastFilter) receiver).owningUid; diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index f4a931f89551..d2af84cf3d30 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -19,7 +19,6 @@ package com.android.server.am; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.am.ActivityManagerService.checkComponentPermission; import static com.android.server.am.BroadcastQueue.TAG; -import static com.android.server.am.Flags.usePermissionManagerForBroadcastDeliveryCheck; import android.annotation.NonNull; import android.annotation.Nullable; @@ -289,33 +288,16 @@ public class BroadcastSkipPolicy { if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID && r.requiredPermissions != null && r.requiredPermissions.length > 0) { - final AttributionSource[] attributionSources; - if (usePermissionManagerForBroadcastDeliveryCheck()) { - attributionSources = createAttributionSourcesForResolveInfo(info); - } else { - attributionSources = null; - } + final AttributionSource[] attributionSources = + createAttributionSourcesForResolveInfo(info); for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; - try { - if (usePermissionManagerForBroadcastDeliveryCheck()) { - perm = hasPermissionForDataDelivery( - requiredPermission, - "Broadcast delivered to " + info.activityInfo.name, - attributionSources) - ? PackageManager.PERMISSION_GRANTED - : PackageManager.PERMISSION_DENIED; - } else { - perm = AppGlobals.getPackageManager() - .checkPermission( - requiredPermission, - info.activityInfo.applicationInfo.packageName, - UserHandle - .getUserId(info.activityInfo.applicationInfo.uid)); - } - } catch (RemoteException e) { - perm = PackageManager.PERMISSION_DENIED; - } + perm = hasPermissionForDataDelivery( + requiredPermission, + "Broadcast delivered to " + info.activityInfo.name, + attributionSources) + ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED; if (perm != PackageManager.PERMISSION_GRANTED) { return "Permission Denial: receiving " + r.intent + " to " @@ -324,15 +306,6 @@ public class BroadcastSkipPolicy { + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"; } - if (!usePermissionManagerForBroadcastDeliveryCheck()) { - int appOp = AppOpsManager.permissionToOpCode(requiredPermission); - if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) { - if (!noteOpForManifestReceiver(appOp, r, info, component)) { - return "Skipping delivery to " + info.activityInfo.packageName - + " due to required appop " + appOp; - } - } - } } } if (r.appOp != AppOpsManager.OP_NONE) { @@ -452,35 +425,20 @@ public class BroadcastSkipPolicy { // Check that the receiver has the required permission(s) to receive this broadcast. if (r.requiredPermissions != null && r.requiredPermissions.length > 0) { - final AttributionSource attributionSource; - if (usePermissionManagerForBroadcastDeliveryCheck()) { - attributionSource = - new AttributionSource.Builder(filter.receiverList.uid) - .setPid(filter.receiverList.pid) - .setPackageName(filter.packageName) - .setAttributionTag(filter.featureId) - .build(); - } else { - attributionSource = null; - } + final AttributionSource attributionSource = + new AttributionSource.Builder(filter.receiverList.uid) + .setPid(filter.receiverList.pid) + .setPackageName(filter.packageName) + .setAttributionTag(filter.featureId) + .build(); for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; - final int perm; - if (usePermissionManagerForBroadcastDeliveryCheck()) { - perm = hasPermissionForDataDelivery( - requiredPermission, - "Broadcast delivered to registered receiver " + filter.receiverId, - attributionSource) - ? PackageManager.PERMISSION_GRANTED - : PackageManager.PERMISSION_DENIED; - } else { - perm = checkComponentPermission( - requiredPermission, - filter.receiverList.pid, - filter.receiverList.uid, - -1 /* owningUid */, - true /* exported */); - } + final int perm = hasPermissionForDataDelivery( + requiredPermission, + "Broadcast delivered to registered receiver " + filter.receiverId, + attributionSource) + ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED; if (perm != PackageManager.PERMISSION_GRANTED) { return "Permission Denial: receiving " + r.intent.toString() @@ -491,24 +449,6 @@ public class BroadcastSkipPolicy { + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"; } - if (!usePermissionManagerForBroadcastDeliveryCheck()) { - int appOp = AppOpsManager.permissionToOpCode(requiredPermission); - if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp - && mService.getAppOpsManager().noteOpNoThrow(appOp, - filter.receiverList.uid, filter.packageName, filter.featureId, - "Broadcast delivered to registered receiver " + filter.receiverId) - != AppOpsManager.MODE_ALLOWED) { - return "Appop Denial: receiving " - + r.intent.toString() - + " to " + filter.receiverList.app - + " (pid=" + filter.receiverList.pid - + ", uid=" + filter.receiverList.uid + ")" - + " requires appop " + AppOpsManager.permissionToOp( - requiredPermission) - + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"; - } - } } } if ((r.requiredPermissions == null || r.requiredPermissions.length == 0)) { diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 8dc7c7345f79..3dd5ec9a3834 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -42,6 +42,7 @@ import android.aconfigd.Aconfigd.StorageReturnMessages; import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon; import static com.android.aconfig_new_storage.Flags.supportImmediateLocalOverrides; import static com.android.aconfig_new_storage.Flags.supportClearLocalOverridesImmediately; +import static com.android.aconfig.flags.Flags.enableSystemAconfigdRust; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -144,7 +145,6 @@ public class SettingsToPropertiesMapper { "android_core_networking", "android_health_services", "android_sdk", - "android_stylus", "aoc", "app_widgets", "arc_next", @@ -209,6 +209,7 @@ public class SettingsToPropertiesMapper { "pixel_continuity", "pixel_perf", "pixel_sensors", + "pixel_state_server", "pixel_system_sw_video", "pixel_video_sw", "pixel_watch", @@ -456,9 +457,11 @@ public class SettingsToPropertiesMapper { static ProtoInputStream sendAconfigdRequests(ProtoOutputStream requests) { // connect to aconfigd socket LocalSocket client = new LocalSocket(); + String socketName = enableSystemAconfigdRust() + ? "aconfigd_system" : "aconfigd"; try{ client.connect(new LocalSocketAddress( - "aconfigd", LocalSocketAddress.Namespace.RESERVED)); + socketName, LocalSocketAddress.Namespace.RESERVED)); Slog.d(TAG, "connected to aconfigd socket"); } catch (IOException ioe) { logErr("failed to connect to aconfigd socket", ioe); diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig index b1185d552941..7f169db7dcec 100644 --- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig +++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig @@ -7,4 +7,12 @@ flag { description: "Restrict priority values defined by non-system apps" is_fixed_read_only: true bug: "369487976" +} + +flag { + name: "limit_priority_scope" + namespace: "backstage_power" + description: "Limit the scope of receiver priorities to within a process" + is_fixed_read_only: true + bug: "369487976" }
\ No newline at end of file diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 09de89445122..34d4fb02ad99 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -40,6 +40,7 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothProfile; import android.content.Intent; +import android.media.AudioDescriptor; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioDevicePort; @@ -47,6 +48,7 @@ import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioManager.AudioDeviceCategory; import android.media.AudioPort; +import android.media.AudioProfile; import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; @@ -619,6 +621,8 @@ public class AudioDeviceInventory { final int mGroupId; @NonNull String mPeerDeviceAddress; @NonNull String mPeerIdentityDeviceAddress; + @NonNull List<AudioProfile> mAudioProfiles; + @NonNull List<AudioDescriptor> mAudioDescriptors; /** Disabled operating modes for this device. Use a negative logic so that by default * an empty list means all modes are allowed. @@ -627,7 +631,8 @@ public class AudioDeviceInventory { DeviceInfo(int deviceType, String deviceName, String address, String identityAddress, int codecFormat, - int groupId, String peerAddress, String peerIdentityAddress) { + int groupId, String peerAddress, String peerIdentityAddress, + List<AudioProfile> profiles, List<AudioDescriptor> descriptors) { mDeviceType = deviceType; mDeviceName = TextUtils.emptyIfNull(deviceName); mDeviceAddress = TextUtils.emptyIfNull(address); @@ -639,6 +644,16 @@ public class AudioDeviceInventory { mGroupId = groupId; mPeerDeviceAddress = TextUtils.emptyIfNull(peerAddress); mPeerIdentityDeviceAddress = TextUtils.emptyIfNull(peerIdentityAddress); + mAudioProfiles = profiles; + mAudioDescriptors = descriptors; + } + + DeviceInfo(int deviceType, String deviceName, String address, + String identityAddress, int codecFormat, + int groupId, String peerAddress, String peerIdentityAddress) { + this(deviceType, deviceName, address, identityAddress, codecFormat, + groupId, peerAddress, peerIdentityAddress, + new ArrayList<>(), new ArrayList<>()); } /** Constructor for all devices except A2DP sink and LE Audio */ @@ -646,6 +661,13 @@ public class AudioDeviceInventory { this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT); } + /** Constructor for HDMI OUT, HDMI ARC/EARC sink devices */ + DeviceInfo(int deviceType, String deviceName, String address, + List<AudioProfile> profiles, List<AudioDescriptor> descriptors) { + this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT, + BluetoothLeAudio.GROUP_ID_INVALID, null, null, profiles, descriptors); + } + /** Constructor for A2DP sink devices */ DeviceInfo(int deviceType, String deviceName, String address, String identityAddress, int codecFormat) { @@ -1194,27 +1216,31 @@ public class AudioDeviceInventory { } /*package*/ void onToggleHdmi() { - MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi") - .set(MediaMetrics.Property.DEVICE, - AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HDMI)); + final int[] hdmiDevices = { AudioSystem.DEVICE_OUT_HDMI, + AudioSystem.DEVICE_OUT_HDMI_ARC, AudioSystem.DEVICE_OUT_HDMI_EARC }; + synchronized (mDevicesLock) { - // Is HDMI connected? - final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HDMI, ""); - final DeviceInfo di = mConnectedDevices.get(key); - if (di == null) { - Log.e(TAG, "invalid null DeviceInfo in onToggleHdmi"); - mmi.set(MediaMetrics.Property.EARLY_RETURN, "invalid null DeviceInfo").record(); - return; + for (DeviceInfo di : mConnectedDevices.values()) { + boolean isHdmiDevice = Arrays.stream(hdmiDevices).anyMatch(device -> + device == di.mDeviceType); + if (isHdmiDevice) { + MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi") + .set(MediaMetrics.Property.DEVICE, + AudioSystem.getDeviceName(di.mDeviceType)); + AudioDeviceAttributes ada = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.convertInternalDeviceToDeviceType(di.mDeviceType), + di.mDeviceAddress, di.mDeviceName, di.mAudioProfiles, + di.mAudioDescriptors); + // Toggle HDMI to retrigger broadcast with proper formats. + setWiredDeviceConnectionState(ada, + AudioSystem.DEVICE_STATE_UNAVAILABLE, "onToggleHdmi"); // disconnect + setWiredDeviceConnectionState(ada, + AudioSystem.DEVICE_STATE_AVAILABLE, "onToggleHdmi"); // reconnect + mmi.record(); + } } - // Toggle HDMI to retrigger broadcast with proper formats. - setWiredDeviceConnectionState( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""), - AudioSystem.DEVICE_STATE_UNAVAILABLE, "android"); // disconnect - setWiredDeviceConnectionState( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""), - AudioSystem.DEVICE_STATE_AVAILABLE, "android"); // reconnect } - mmi.record(); } @GuardedBy("mDevicesLock") @@ -1818,7 +1844,15 @@ public class AudioDeviceInventory { .printSlog(EventLogger.Event.ALOGE, TAG)); return false; } - mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address)); + + if (device == AudioSystem.DEVICE_OUT_HDMI || + device == AudioSystem.DEVICE_OUT_HDMI_ARC || + device == AudioSystem.DEVICE_OUT_HDMI_EARC) { + mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, + address, attributes.getAudioProfiles(), attributes.getAudioDescriptors())); + } else { + mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address)); + } mDeviceBroker.postAccessoryPlugMediaUnmute(device); status = true; } else if (!connect && isConnected) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 0cf55bb2bf17..6ba356990cac 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -9193,16 +9193,6 @@ public class AudioService extends IAudioService.Stub mIndexMin = MIN_STREAM_VOLUME[streamType] * 10; mIndexMax = MAX_STREAM_VOLUME[streamType] * 10; - final int status = AudioSystem.initStreamVolume( - streamType, MIN_STREAM_VOLUME[streamType], MAX_STREAM_VOLUME[streamType]); - if (status != AudioSystem.AUDIO_STATUS_OK) { - sLifecycleLogger.enqueue(new EventLogger.StringEvent( - "VSS() stream:" + streamType + " initStreamVolume=" + status) - .printLog(ALOGE, TAG)); - sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0, - "VSS()" /*obj*/, 2 * INDICATE_SYSTEM_READY_RETRY_DELAY_MS); - } - updateIndexFactors(); mIndexMinNoPerm = mIndexMin; // may be overwritten later in updateNoPermMinIndex() @@ -9267,6 +9257,19 @@ public class AudioService extends IAudioService.Stub mIndexMinNoPerm = mIndexMin; } } + + final int status = AudioSystem.initStreamVolume( + mStreamType, mIndexMin / 10, mIndexMax / 10); + sVolumeLogger.enqueue(new EventLogger.StringEvent( + "updateIndexFactors() stream:" + mStreamType + " index min/max:" + + mIndexMin / 10 + "/" + mIndexMax / 10 + " indexStepFactor:" + + mIndexStepFactor).printSlog(ALOGI, TAG)); + if (status != AudioSystem.AUDIO_STATUS_OK) { + sVolumeLogger.enqueue(new EventLogger.StringEvent( + "Failed initStreamVolume with status=" + status).printSlog(ALOGE, TAG)); + sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0, + "updateIndexFactors()" /*obj*/, 2 * INDICATE_SYSTEM_READY_RETRY_DELAY_MS); + } } /** diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 00280c8f9c04..65780238ede4 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -288,11 +288,13 @@ public class BiometricService extends SystemService { * @param handler The handler to run {@link #onChange} on, or null if none. */ public SettingObserver(Context context, Handler handler, - List<BiometricService.EnabledOnKeyguardCallback> callbacks) { + List<BiometricService.EnabledOnKeyguardCallback> callbacks, + UserManager userManager, FingerprintManager fingerprintManager, + FaceManager faceManager) { super(handler); mContentResolver = context.getContentResolver(); mCallbacks = callbacks; - mUserManager = context.getSystemService(UserManager.class); + mUserManager = userManager; final boolean hasFingerprint = context.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT); @@ -304,7 +306,7 @@ public class BiometricService extends SystemService { Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.Q && hasFace && !hasFingerprint; - addBiometricListenersForMandatoryBiometrics(context); + addBiometricListenersForMandatoryBiometrics(context, fingerprintManager, faceManager); updateContentObserver(); } @@ -431,11 +433,21 @@ public class BiometricService extends SystemService { public boolean getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(int userId) { if (!mMandatoryBiometricsEnabled.containsKey(userId)) { + Slog.d(TAG, "update mb toggle for user " + userId); updateMandatoryBiometricsForAllProfiles(userId); } if (!mMandatoryBiometricsRequirementsSatisfied.containsKey(userId)) { + Slog.d(TAG, "update mb reqs for user " + userId); updateMandatoryBiometricsRequirementsForAllProfiles(userId); } + + Slog.d(TAG, mMandatoryBiometricsEnabled.getOrDefault(userId, + DEFAULT_MANDATORY_BIOMETRICS_STATUS) + + " " + mMandatoryBiometricsRequirementsSatisfied.getOrDefault(userId, + DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS) + + " " + getEnabledForApps(userId) + + " " + (mFingerprintEnrolledForUser.getOrDefault(userId, false /* default */) + || mFaceEnrolledForUser.getOrDefault(userId, false /* default */))); return mMandatoryBiometricsEnabled.getOrDefault(userId, DEFAULT_MANDATORY_BIOMETRICS_STATUS) && mMandatoryBiometricsRequirementsSatisfied.getOrDefault(userId, @@ -456,11 +468,23 @@ public class BiometricService extends SystemService { private void updateMandatoryBiometricsForAllProfiles(int userId) { int effectiveUserId = userId; - if (mUserManager.getMainUser() != null) { - effectiveUserId = mUserManager.getMainUser().getIdentifier(); + final UserInfo parentProfile = mUserManager.getProfileParent(userId); + + if (parentProfile != null) { + effectiveUserId = parentProfile.id; } - for (int profileUserId: mUserManager.getEnabledProfileIds(effectiveUserId)) { - mMandatoryBiometricsEnabled.put(profileUserId, + + final int[] enabledProfileIds = mUserManager.getEnabledProfileIds(effectiveUserId); + if (enabledProfileIds != null) { + for (int profileUserId : enabledProfileIds) { + mMandatoryBiometricsEnabled.put(profileUserId, + Settings.Secure.getIntForUser( + mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS, + DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0, + effectiveUserId) != 0); + } + } else { + mMandatoryBiometricsEnabled.put(userId, Settings.Secure.getIntForUser( mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS, DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0, @@ -470,11 +494,24 @@ public class BiometricService extends SystemService { private void updateMandatoryBiometricsRequirementsForAllProfiles(int userId) { int effectiveUserId = userId; - if (mUserManager.getMainUser() != null) { - effectiveUserId = mUserManager.getMainUser().getIdentifier(); + final UserInfo parentProfile = mUserManager.getProfileParent(userId); + + if (parentProfile != null) { + effectiveUserId = parentProfile.id; } - for (int profileUserId: mUserManager.getEnabledProfileIds(effectiveUserId)) { - mMandatoryBiometricsRequirementsSatisfied.put(profileUserId, + + final int[] enabledProfileIds = mUserManager.getEnabledProfileIds(effectiveUserId); + if (enabledProfileIds != null) { + for (int profileUserId : enabledProfileIds) { + mMandatoryBiometricsRequirementsSatisfied.put(profileUserId, + Settings.Secure.getIntForUser(mContentResolver, + Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, + DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS + ? 1 : 0, + effectiveUserId) != 0); + } + } else { + mMandatoryBiometricsRequirementsSatisfied.put(userId, Settings.Secure.getIntForUser(mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS ? 1 : 0, @@ -482,10 +519,8 @@ public class BiometricService extends SystemService { } } - private void addBiometricListenersForMandatoryBiometrics(Context context) { - final FingerprintManager fingerprintManager = context.getSystemService( - FingerprintManager.class); - final FaceManager faceManager = context.getSystemService(FaceManager.class); + private void addBiometricListenersForMandatoryBiometrics(Context context, + FingerprintManager fingerprintManager, FaceManager faceManager) { if (fingerprintManager != null) { fingerprintManager.addAuthenticatorsRegisteredCallback( new IFingerprintAuthenticatorsRegisteredCallback.Stub() { @@ -1169,7 +1204,9 @@ public class BiometricService extends SystemService { */ public SettingObserver getSettingObserver(Context context, Handler handler, List<EnabledOnKeyguardCallback> callbacks) { - return new SettingObserver(context, handler, callbacks); + return new SettingObserver(context, handler, callbacks, context.getSystemService( + UserManager.class), context.getSystemService(FingerprintManager.class), + context.getSystemService(FaceManager.class)); } /** 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 c0aa4cc6fa24..71f17d1f411e 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -242,6 +242,11 @@ public class DisplayManagerFlags { Flags::autoBrightnessModeBedtimeWear ); + private final FlagState mEnablePluginManagerFlagState = new FlagState( + Flags.FLAG_ENABLE_PLUGIN_MANAGER, + Flags::enablePluginManager + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -517,6 +522,10 @@ public class DisplayManagerFlags { return mAutoBrightnessModeBedtimeWearFlagState.isEnabled(); } + public boolean isPluginManagerEnabled() { + return mEnablePluginManagerFlagState.isEnabled(); + } + /** * dumps all flagstates * @param pw printWriter @@ -568,6 +577,7 @@ public class DisplayManagerFlags { pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled); pw.println(" " + mHasArrSupport); pw.println(" " + mAutoBrightnessModeBedtimeWearFlagState); + pw.println(" " + mEnablePluginManagerFlagState); } private static class FlagState { 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 a9bdccef2300..7850360c7dbf 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 @@ -446,3 +446,11 @@ flag { bug: "365163968" is_fixed_read_only: true } + +flag { + name: "enable_plugin_manager" + namespace: "display_manager" + description: "Flag to enable DisplayManager plugins" + bug: "354059797" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 76e5ef011789..0c04be10d06d 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.BIND_DREAM_SERVICE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.service.dreams.Flags.cleanupDreamSettingsOnUninstall; import static android.service.dreams.Flags.dreamHandlesBeingObscured; import static com.android.server.wm.ActivityInterceptorCallback.DREAM_MANAGER_ORDERED_ID; @@ -64,12 +65,15 @@ import android.provider.Settings; import android.service.dreams.DreamManagerInternal; import android.service.dreams.DreamService; import android.service.dreams.IDreamManager; +import android.text.TextUtils; import android.util.Slog; +import android.util.SparseArray; import android.view.Display; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.content.PackageMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.util.DumpUtils; @@ -86,6 +90,7 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; @@ -155,6 +160,10 @@ public final class DreamManagerService extends SystemService { private ComponentName mDreamOverlayServiceName; private final AmbientDisplayConfiguration mDozeConfig; + + /** Stores {@link PerUserPackageMonitor} to monitor dream uninstalls. */ + private final SparseArray<PackageMonitor> mPackageMonitors = new SparseArray<>(); + private final ActivityInterceptorCallback mActivityInterceptorCallback = new ActivityInterceptorCallback() { @Nullable @@ -218,6 +227,15 @@ public final class DreamManagerService extends SystemService { } } + private final class PerUserPackageMonitor extends PackageMonitor { + @Override + public void onPackageRemoved(String packageName, int uid) { + super.onPackageRemoved(packageName, uid); + final int userId = getChangingUserId(); + updateDreamOnPackageRemoved(packageName, userId); + } + } + public DreamManagerService(Context context) { this(context, new DreamHandler(FgThread.get().getLooper())); } @@ -333,6 +351,37 @@ public final class DreamManagerService extends SystemService { }); } + @Override + public void onUserStarting(@NonNull TargetUser user) { + super.onUserStarting(user); + if (cleanupDreamSettingsOnUninstall()) { + mHandler.post(() -> { + final int userId = user.getUserIdentifier(); + if (!mPackageMonitors.contains(userId)) { + final PackageMonitor monitor = new PerUserPackageMonitor(); + monitor.register(mContext, UserHandle.of(userId), mHandler); + mPackageMonitors.put(userId, monitor); + } else { + Slog.w(TAG, "Package monitor already registered for " + userId); + } + }); + } + } + + @Override + public void onUserStopping(@NonNull TargetUser user) { + super.onUserStopping(user); + if (cleanupDreamSettingsOnUninstall()) { + mHandler.post(() -> { + final PackageMonitor monitor = mPackageMonitors.removeReturnOld( + user.getUserIdentifier()); + if (monitor != null) { + monitor.unregister(); + } + }); + } + } + private void dumpInternal(PrintWriter pw) { synchronized (mLock) { pw.println("DREAM MANAGER (dumpsys dreams)"); @@ -664,6 +713,30 @@ public final class DreamManagerService extends SystemService { return validComponents.toArray(new ComponentName[validComponents.size()]); } + private void updateDreamOnPackageRemoved(String packageName, int userId) { + final ComponentName[] componentNames = componentsFromString( + Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.SCREENSAVER_COMPONENTS, + userId)); + if (componentNames != null) { + // Filter out any components in the removed package. + final ComponentName[] filteredComponents = + Arrays.stream(componentNames) + .filter((componentName -> !isSamePackage(packageName, componentName))) + .toArray(ComponentName[]::new); + if (filteredComponents.length != componentNames.length) { + setDreamComponentsForUser(userId, filteredComponents); + } + } + } + + private static boolean isSamePackage(String packageName, ComponentName componentName) { + if (packageName == null || componentName == null) { + return false; + } + return TextUtils.equals(componentName.getPackageName(), packageName); + } + private void setDreamComponentsForUser(int userId, ComponentName[] componentNames) { Settings.Secure.putStringForUser(mContext.getContentResolver(), Settings.Secure.SCREENSAVER_COMPONENTS, @@ -824,7 +897,10 @@ public final class DreamManagerService extends SystemService { } StringBuilder names = new StringBuilder(); for (ComponentName componentName : componentNames) { - if (names.length() > 0) { + if (componentName == null) { + continue; + } + if (!names.isEmpty()) { names.append(','); } names.append(componentName.flattenToString()); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java index f2e2f653f929..5b4c0337862b 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecController.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java @@ -1001,7 +1001,7 @@ final class HdmiCecController { try { // Create an AIDL callback that can callback onHotplugEvent mHdmiConnection.setCallback(new HdmiConnectionCallbackAidl(callback)); - } catch (RemoteException e) { + } catch (RemoteException | NullPointerException e) { HdmiLogger.error("Couldn't initialise tv.hdmi callback : ", e); } } @@ -1134,7 +1134,7 @@ final class HdmiCecController { i++; } return hdmiPortInfo; - } catch (RemoteException e) { + } catch (RemoteException | NullPointerException e) { HdmiLogger.error("Failed to get port information : ", e); return null; } @@ -1144,7 +1144,7 @@ final class HdmiCecController { public boolean nativeIsConnected(int port) { try { return mHdmiConnection.isConnected(port); - } catch (RemoteException e) { + } catch (RemoteException | NullPointerException e) { HdmiLogger.error("Failed to get connection info : ", e); return false; } @@ -1158,7 +1158,7 @@ final class HdmiCecController { HdmiLogger.error( "Could not set HPD signal type for portId " + portId + " to " + signal + ". Error: ", sse.errorCode); - } catch (RemoteException e) { + } catch (RemoteException | NullPointerException e) { HdmiLogger.error( "Could not set HPD signal type for portId " + portId + " to " + signal + ". Exception: ", e); @@ -1169,7 +1169,7 @@ final class HdmiCecController { public int nativeGetHpdSignalType(int portId) { try { return mHdmiConnection.getHpdSignal(portId); - } catch (RemoteException e) { + } catch (RemoteException | NullPointerException e) { HdmiLogger.error( "Could not get HPD signal type for portId " + portId + ". Exception: ", e); return Constants.HDMI_HPD_TYPE_PHYSICAL; diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index bf415a344f4c..7505c710f483 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -646,9 +646,9 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { int address = message.getSource(); int type = message.getParams()[2]; - if (!ActiveSource.of(address, path).equals(getActiveSource())) { - HdmiLogger.debug("Check if a new device is connected to the active path"); - handleNewDeviceAtTheTailOfActivePath(path); + if (getActiveSource().logicalAddress != address && getActivePath() == path) { + HdmiLogger.debug("New logical address detected on the current active path."); + startRoutingControl(path, path, null); } startNewDeviceAction(ActiveSource.of(address, path), type); return Constants.HANDLED; diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 132d6fa377eb..0c5069f81774 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -771,6 +771,14 @@ public class HdmiControlService extends SystemService { Slog.i(TAG, "Device does not support eARC."); } mHdmiCecNetwork = new HdmiCecNetwork(this, mCecController, mMhlController); + if (isTvDevice() && getWasCecDisabledOnStandbyByLowEnergyMode()) { + Slog.w(TAG, "Re-enable CEC on boot-up since it was disabled due to low energy " + + " mode."); + getHdmiCecConfig().setIntValue(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED, + HDMI_CEC_CONTROL_ENABLED); + setWasCecDisabledOnStandbyByLowEnergyMode(false); + setCecEnabled(HDMI_CEC_CONTROL_ENABLED); + } if (isCecControlEnabled()) { initializeCec(INITIATED_BY_BOOT_UP); } else { diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index e545dd507f28..6f3540221b63 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -215,6 +215,18 @@ final class InputGestureManager { systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_T, KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK)); + systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_MINUS, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT)); + systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_EQUALS, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN)); + systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_M, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION)); + systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_S, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK)); } if (keyboardA11yShortcutControl()) { if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) { diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 155ffe805519..0124e25f0655 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -20,6 +20,7 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_WATCH; import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; +import static com.android.hardware.input.Flags.enableNew25q2Keycodes; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures; @@ -750,6 +751,28 @@ final class KeyGestureController { } } break; + case KeyEvent.KEYCODE_LOCK: + if (enableNew25q2Keycodes()) { + if (firstDown) { + handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_LOCK}, + /* modifierState = */0, + KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, + /* flags = */0, /* appLaunchData = */null); + } + } + return true; + case KeyEvent.KEYCODE_FULLSCREEN: + if (enableNew25q2Keycodes()) { + if (firstDown) { + handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_FULLSCREEN}, + /* modifierState = */0, + KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, + /* flags = */0, /* appLaunchData = */null); + } + } + return true; case KeyEvent.KEYCODE_ASSIST: Slog.wtf(TAG, "KEYCODE_ASSIST should be handled in interceptKeyBeforeQueueing"); return true; @@ -764,6 +787,9 @@ final class KeyGestureController { Slog.wtf(TAG, "KEYCODE_STYLUS_BUTTON_* should be handled in" + " interceptKeyBeforeQueueing"); return true; + case KeyEvent.KEYCODE_DO_NOT_DISTURB: + // TODO(b/365920375): Implement 25Q2 keycode implementation in system + return true; } // Handle custom shortcuts diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index a45ea1d8369d..21ae1820f6f7 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -59,15 +59,15 @@ public class MediaQualityService extends SystemService { return pp; } @Override - public void updatePictureProfile(long id, PictureProfile pp) { + public void updatePictureProfile(String id, PictureProfile pp) { // TODO: implement } @Override - public void removePictureProfile(long id) { + public void removePictureProfile(String id) { // TODO: implement } @Override - public PictureProfile getPictureProfileById(long id) { + public PictureProfile getPictureProfile(int type, String name) { return null; } @Override @@ -79,7 +79,7 @@ public class MediaQualityService extends SystemService { return new ArrayList<>(); } @Override - public List<PictureProfile> getAllPictureProfiles() { + public List<String> getPictureProfilePackageNames() { return new ArrayList<>(); } @@ -89,15 +89,15 @@ public class MediaQualityService extends SystemService { return pp; } @Override - public void updateSoundProfile(long id, SoundProfile pp) { + public void updateSoundProfile(String id, SoundProfile pp) { // TODO: implement } @Override - public void removeSoundProfile(long id) { + public void removeSoundProfile(String id) { // TODO: implement } @Override - public SoundProfile getSoundProfileById(long id) { + public SoundProfile getSoundProfileById(String id) { return null; } @Override @@ -109,7 +109,7 @@ public class MediaQualityService extends SystemService { return new ArrayList<>(); } @Override - public List<SoundProfile> getAllSoundProfiles() { + public List<String> getSoundProfilePackageNames() { return new ArrayList<>(); } @@ -138,6 +138,14 @@ public class MediaQualityService extends SystemService { return new ArrayList<>(); } + @Override + public List<String> getPictureProfileAllowList() { + return new ArrayList<>(); + } + + @Override + public void setPictureProfileAllowList(List<String> packages) { + } @Override public boolean isSupported() { diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 2a3be1e119bf..7de2815eba6b 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -513,12 +513,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { private boolean mLoadedRestrictBackground; /** - * Whether or not network for apps in proc-states greater than - * {@link NetworkPolicyManager#BACKGROUND_THRESHOLD_STATE} is always blocked. - */ - private boolean mBackgroundNetworkRestricted; - - /** * Whether or not metered firewall chains should be used for uid policy controlling access to * metered networks. */ @@ -1117,14 +1111,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { writePolicyAL(); } - // The flag is boot-stable. - mBackgroundNetworkRestricted = Flags.networkBlockedForTopSleepingAndAbove(); - if (mBackgroundNetworkRestricted) { - // Firewall rules and UidBlockedState will get updated in - // updateRulesForGlobalChangeAL below. - enableFirewallChainUL(FIREWALL_CHAIN_BACKGROUND, true); - } - + enableFirewallChainUL(FIREWALL_CHAIN_BACKGROUND, true); setRestrictBackgroundUL(mLoadedRestrictBackground, "init_service"); updateRulesForGlobalChangeAL(false); updateNotificationsNL(); @@ -1135,11 +1122,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { final int changes = ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_CAPABILITY; - - final int cutpoint = mBackgroundNetworkRestricted ? PROCESS_STATE_UNKNOWN - : NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE; mActivityManagerInternal.registerNetworkPolicyUidObserver(mUidObserver, changes, - cutpoint, "android"); + PROCESS_STATE_UNKNOWN, "android"); mNetworkManager.registerObserver(mAlertObserver); } catch (RemoteException e) { // ignored; both services live in system_server @@ -1280,21 +1264,19 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // different chains may change. return true; } - if (mBackgroundNetworkRestricted) { - if ((previousProcState >= BACKGROUND_THRESHOLD_STATE) + if ((previousProcState >= BACKGROUND_THRESHOLD_STATE) != (newProcState >= BACKGROUND_THRESHOLD_STATE)) { - // Proc-state change crossed BACKGROUND_THRESHOLD_STATE: The network rules will - // need to be re-evaluated for the background chain. - return true; - } - if (mUseDifferentDelaysForBackgroundChain - && newProcState >= BACKGROUND_THRESHOLD_STATE - && getBackgroundTransitioningDelay(newProcState) - < getBackgroundTransitioningDelay(previousProcState)) { - // The old and new proc-state both are in the blocked state but the background - // transition delay is reduced, so we may have to update the rules sooner. - return true; - } + // Proc-state change crossed BACKGROUND_THRESHOLD_STATE: The network rules will + // need to be re-evaluated for the background chain. + return true; + } + if (mUseDifferentDelaysForBackgroundChain + && newProcState >= BACKGROUND_THRESHOLD_STATE + && getBackgroundTransitioningDelay(newProcState) + < getBackgroundTransitioningDelay(previousProcState)) { + // The old and new proc-state both are in the blocked state but the background + // transition delay is reduced, so we may have to update the rules sooner. + return true; } final int networkCapabilities = PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK | PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK; @@ -1367,9 +1349,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // on background handler thread, and POWER_SAVE_WHITELIST_CHANGED is protected synchronized (mUidRulesFirstLock) { updatePowerSaveAllowlistUL(); - if (mBackgroundNetworkRestricted) { - updateRulesForBackgroundChainUL(); - } + updateRulesForBackgroundChainUL(); updateRulesForRestrictPowerUL(); updateRulesForAppIdleUL(); } @@ -4100,8 +4080,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { fout.println(); fout.println("Flags:"); - fout.println(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE + ": " - + mBackgroundNetworkRestricted); fout.println(Flags.FLAG_USE_METERED_FIREWALL_CHAINS + ": " + mUseMeteredFirewallChains); fout.println(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN + ": " @@ -4251,35 +4229,33 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { fout.decreaseIndent(); } - if (mBackgroundNetworkRestricted) { + fout.println(); + if (mUseDifferentDelaysForBackgroundChain) { + fout.print("Background restrictions short delay: "); + TimeUtils.formatDuration(mBackgroundRestrictionShortDelayMs, fout); fout.println(); - if (mUseDifferentDelaysForBackgroundChain) { - fout.print("Background restrictions short delay: "); - TimeUtils.formatDuration(mBackgroundRestrictionShortDelayMs, fout); - fout.println(); - fout.print("Background restrictions long delay: "); - TimeUtils.formatDuration(mBackgroundRestrictionLongDelayMs, fout); - fout.println(); - } + fout.print("Background restrictions long delay: "); + TimeUtils.formatDuration(mBackgroundRestrictionLongDelayMs, fout); + fout.println(); + } - size = mBackgroundTransitioningUids.size(); - if (size > 0) { - final long nowUptime = SystemClock.uptimeMillis(); - fout.println("Uids transitioning to background:"); - fout.increaseIndent(); - for (int i = 0; i < size; i++) { - fout.print("UID="); - fout.print(mBackgroundTransitioningUids.keyAt(i)); - fout.print(", "); - TimeUtils.formatDuration(mBackgroundTransitioningUids.valueAt(i), - nowUptime, fout); - fout.println(); - } - fout.decreaseIndent(); + size = mBackgroundTransitioningUids.size(); + if (size > 0) { + final long nowUptime = SystemClock.uptimeMillis(); + fout.println("Uids transitioning to background:"); + fout.increaseIndent(); + for (int i = 0; i < size; i++) { + fout.print("UID="); + fout.print(mBackgroundTransitioningUids.keyAt(i)); + fout.print(", "); + TimeUtils.formatDuration(mBackgroundTransitioningUids.valueAt(i), + nowUptime, fout); + fout.println(); } - fout.println(); + fout.decreaseIndent(); } + fout.println(); final SparseBooleanArray knownUids = new SparseBooleanArray(); collectKeys(mUidState, knownUids); @@ -4465,51 +4441,49 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } updatePowerRestrictionRules = true; } - if (mBackgroundNetworkRestricted) { - final boolean wasAllowed = isProcStateAllowedNetworkWhileBackground( - oldUidState); - final boolean isAllowed = isProcStateAllowedNetworkWhileBackground(newUidState); - if (!wasAllowed && isAllowed) { - mBackgroundTransitioningUids.delete(uid); - updateRuleForBackgroundUL(uid); - updatePowerRestrictionRules = true; - } else if (!isAllowed) { - final int transitionIdx = mBackgroundTransitioningUids.indexOfKey(uid); - final long completionTimeMs = SystemClock.uptimeMillis() - + getBackgroundTransitioningDelay(procState); - boolean completionTimeUpdated = false; - if (wasAllowed) { - // Rules need to transition from allowed to blocked after the respective - // delay. - if (transitionIdx < 0) { - // This is just a defensive check in case the upstream code ever - // makes multiple calls for the same process state change. - mBackgroundTransitioningUids.put(uid, completionTimeMs); - completionTimeUpdated = true; - } - } else if (mUseDifferentDelaysForBackgroundChain) { - // wasAllowed was false, but the transition delay may have reduced. - // Currently, this can happen when the uid transitions from - // LAST_ACTIVITY to CACHED_ACTIVITY, for example. - if (transitionIdx >= 0 - && completionTimeMs < mBackgroundTransitioningUids.valueAt( - transitionIdx)) { - mBackgroundTransitioningUids.setValueAt(transitionIdx, - completionTimeMs); - completionTimeUpdated = true; - } + final boolean wasAllowed = isProcStateAllowedNetworkWhileBackground( + oldUidState); + final boolean isAllowed = isProcStateAllowedNetworkWhileBackground(newUidState); + if (!wasAllowed && isAllowed) { + mBackgroundTransitioningUids.delete(uid); + updateRuleForBackgroundUL(uid); + updatePowerRestrictionRules = true; + } else if (!isAllowed) { + final int transitionIdx = mBackgroundTransitioningUids.indexOfKey(uid); + final long completionTimeMs = SystemClock.uptimeMillis() + + getBackgroundTransitioningDelay(procState); + boolean completionTimeUpdated = false; + if (wasAllowed) { + // Rules need to transition from allowed to blocked after the respective + // delay. + if (transitionIdx < 0) { + // This is just a defensive check in case the upstream code ever + // makes multiple calls for the same process state change. + mBackgroundTransitioningUids.put(uid, completionTimeMs); + completionTimeUpdated = true; } - if (completionTimeUpdated - && completionTimeMs < mNextProcessBackgroundUidsTime) { - // Many uids may be in this "transitioning" state at the same time, - // so we always keep one message to process transition completion at - // the earliest time. - mHandler.removeMessages(MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS); - mHandler.sendEmptyMessageAtTime( - MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS, completionTimeMs); - mNextProcessBackgroundUidsTime = completionTimeMs; + } else if (mUseDifferentDelaysForBackgroundChain) { + // wasAllowed was false, but the transition delay may have reduced. + // Currently, this can happen when the uid transitions from + // LAST_ACTIVITY to CACHED_ACTIVITY, for example. + if (transitionIdx >= 0 + && completionTimeMs < mBackgroundTransitioningUids.valueAt( + transitionIdx)) { + mBackgroundTransitioningUids.setValueAt(transitionIdx, + completionTimeMs); + completionTimeUpdated = true; } } + if (completionTimeUpdated + && completionTimeMs < mNextProcessBackgroundUidsTime) { + // Many uids may be in this "transitioning" state at the same time, + // so we always keep one message to process transition completion at + // the earliest time. + mHandler.removeMessages(MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS); + mHandler.sendEmptyMessageAtTime( + MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS, completionTimeMs); + mNextProcessBackgroundUidsTime = completionTimeMs; + } } if (mLowPowerStandbyActive) { boolean allowedInLpsChanged = @@ -4545,12 +4519,10 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { if (mRestrictPower) { updateRuleForRestrictPowerUL(uid); } - if (mBackgroundNetworkRestricted) { - // Uid is no longer running, there is no point in any grace period of network - // access during transitions to lower importance proc-states. - mBackgroundTransitioningUids.delete(uid); - updateRuleForBackgroundUL(uid); - } + // Uid is no longer running, there is no point in any grace period of network + // access during transitions to lower importance proc-states. + mBackgroundTransitioningUids.delete(uid); + updateRuleForBackgroundUL(uid); updateRulesForPowerRestrictionsUL(uid); if (mLowPowerStandbyActive) { updateRuleForLowPowerStandbyUL(uid); @@ -5021,9 +4993,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { "updateRulesForGlobalChangeAL: " + (restrictedNetworksChanged ? "R" : "-")); } try { - if (mBackgroundNetworkRestricted) { - updateRulesForBackgroundChainUL(); - } + updateRulesForBackgroundChainUL(); updateRulesForAppIdleUL(); updateRulesForRestrictPowerUL(); updateRulesForRestrictBackgroundUL(); @@ -5183,9 +5153,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN); updateRuleForDeviceIdleUL(uid); updateRuleForRestrictPowerUL(uid); - if (mBackgroundNetworkRestricted) { - updateRuleForBackgroundUL(uid); - } + updateRuleForBackgroundUL(uid); // Update internal rules. updateRulesForPowerRestrictionsUL(uid); } @@ -5358,9 +5326,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { updateRuleForDeviceIdleUL(uid); updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN); updateRuleForRestrictPowerUL(uid); - if (mBackgroundNetworkRestricted) { - updateRuleForBackgroundUL(uid); - } + updateRuleForBackgroundUL(uid); // If the uid has the necessary permissions, then it should be added to the restricted mode // firewall allowlist. @@ -5611,7 +5577,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { newBlockedReasons |= (mLowPowerStandbyActive ? BLOCKED_REASON_LOW_POWER_STANDBY : 0); newBlockedReasons |= (isUidIdle ? BLOCKED_REASON_APP_STANDBY : 0); newBlockedReasons |= (uidBlockedState.blockedReasons & BLOCKED_REASON_RESTRICTED_MODE); - newBlockedReasons |= mBackgroundNetworkRestricted ? BLOCKED_REASON_APP_BACKGROUND : 0; + newBlockedReasons |= BLOCKED_REASON_APP_BACKGROUND; newAllowedReasons |= (isSystem(uid) ? ALLOWED_REASON_SYSTEM : 0); newAllowedReasons |= (isForeground ? ALLOWED_REASON_FOREGROUND : 0); @@ -5624,8 +5590,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { & ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS); newAllowedReasons |= (isAllowlistedFromLowPowerStandbyUL(uid)) ? ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST : 0; - newAllowedReasons |= (mBackgroundNetworkRestricted - && isUidExemptFromBackgroundRestrictions(uid)) + newAllowedReasons |= isUidExemptFromBackgroundRestrictions(uid) ? ALLOWED_REASON_NOT_IN_BACKGROUND : 0; uidBlockedState.blockedReasons = (uidBlockedState.blockedReasons diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig index 7f04e665567e..3c0ff6115fcd 100644 --- a/services/core/java/com/android/server/net/flags.aconfig +++ b/services/core/java/com/android/server/net/flags.aconfig @@ -2,13 +2,6 @@ package: "com.android.server.net" container: "system" flag { - name: "network_blocked_for_top_sleeping_and_above" - namespace: "backstage_power" - description: "Block network access for apps in a low importance background state" - bug: "304347838" -} - -flag { name: "use_metered_firewall_chains" namespace: "backstage_power" description: "Use metered firewall chains to control access to metered networks" diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index 5febd5c07e3a..5914dbe44b0b 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -788,6 +788,20 @@ public class GroupHelper { return; } + // Check if summary & child notifications are not part of the same section/bundle + // Needs a check here if notification was bundled while enqueued + if (notificationRegroupOnClassification() + && android.service.notification.Flags.notificationClassification()) { + if (isGroupChildBundled(record, summaryByGroupKey)) { + if (DEBUG) { + Slog.v(TAG, "isGroupChildInDifferentBundleThanSummary: " + record); + } + moveNotificationsToNewSection(record.getUserId(), pkgName, + List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey))); + return; + } + } + // scenario 3: sparse/singleton groups if (Flags.notificationForceGroupSingletons()) { try { @@ -800,6 +814,27 @@ public class GroupHelper { } } + private static boolean isGroupChildBundled(final NotificationRecord record, + final Map<String, NotificationRecord> summaryByGroupKey) { + final StatusBarNotification sbn = record.getSbn(); + final String groupKey = record.getSbn().getGroupKey(); + + if (!sbn.isAppGroup()) { + return false; + } + + if (record.getNotification().isGroupSummary()) { + return false; + } + + final NotificationRecord summary = summaryByGroupKey.get(groupKey); + if (summary == null) { + return false; + } + + return NotificationChannel.SYSTEM_RESERVED_IDS.contains(record.getChannel().getId()); + } + /** * Called when a notification is removed, so that this helper can adjust the aggregate groups: * - Removes the autogroup summary of the notification's section @@ -1598,7 +1633,7 @@ public class GroupHelper { final int mSummaryId; private final Predicate<NotificationRecord> mSectionChecker; - public NotificationSectioner(String name, int summaryId, + private NotificationSectioner(String name, int summaryId, Predicate<NotificationRecord> sectionChecker) { mName = name; mSummaryId = summaryId; diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 93482e769a2b..b0ef80793cd7 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -75,9 +75,7 @@ import com.android.internal.util.XmlUtils; import com.android.internal.util.function.TriPredicate; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; -import com.android.server.LocalServices; import com.android.server.notification.NotificationManagerService.DumpFilter; -import com.android.server.pm.UserManagerInternal; import com.android.server.utils.TimingsTraceAndSlog; import org.xmlpull.v1.XmlPullParser; @@ -136,7 +134,6 @@ abstract public class ManagedServices { private final UserProfiles mUserProfiles; protected final IPackageManager mPm; protected final UserManager mUm; - private final UserManagerInternal mUserManagerInternal; private final Config mConfig; private final Handler mHandler = new Handler(Looper.getMainLooper()); @@ -198,7 +195,6 @@ abstract public class ManagedServices { mConfig = getConfig(); mApprovalLevel = APPROVAL_BY_COMPONENT; mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); } abstract protected Config getConfig(); @@ -1389,14 +1385,9 @@ abstract public class ManagedServices { @GuardedBy("mMutex") protected void populateComponentsToBind(SparseArray<Set<ComponentName>> componentsToBind, final IntArray activeUsers, - SparseArray<ArraySet<ComponentName>> approvedComponentsByUser, - boolean isVisibleBackgroundUser) { - // When it is a visible background user in Automotive MUMD environment, - // don't clear mEnabledServicesForCurrentProfile and mEnabledServicesPackageNames. - if (!isVisibleBackgroundUser) { - mEnabledServicesForCurrentProfiles.clear(); - mEnabledServicesPackageNames.clear(); - } + SparseArray<ArraySet<ComponentName>> approvedComponentsByUser) { + mEnabledServicesForCurrentProfiles.clear(); + mEnabledServicesPackageNames.clear(); final int nUserIds = activeUsers.size(); for (int i = 0; i < nUserIds; ++i) { @@ -1417,12 +1408,7 @@ abstract public class ManagedServices { } componentsToBind.put(userId, add); - // When it is a visible background user in Automotive MUMD environment, - // skip adding items to mEnabledServicesForCurrentProfile - // and mEnabledServicesPackageNames. - if (isVisibleBackgroundUser) { - continue; - } + mEnabledServicesForCurrentProfiles.addAll(userComponents); for (int j = 0; j < userComponents.size(); j++) { @@ -1470,10 +1456,7 @@ abstract public class ManagedServices { IntArray userIds = mUserProfiles.getCurrentProfileIds(); boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind, mContext) && allowRebindForParentUser(); - boolean isVisibleBackgroundUser = false; if (userToRebind != USER_ALL && !rebindAllCurrentUsers) { - isVisibleBackgroundUser = - mUserManagerInternal.isVisibleBackgroundFullUser(userToRebind); userIds = new IntArray(1); userIds.add(userToRebind); } @@ -1488,8 +1471,7 @@ abstract public class ManagedServices { // Filter approvedComponentsByUser to collect all of the components that are allowed // for the currently active user(s). - populateComponentsToBind(componentsToBind, userIds, approvedComponentsByUser, - isVisibleBackgroundUser); + populateComponentsToBind(componentsToBind, userIds, approvedComponentsByUser); // For every current non-system connection, disconnect services that are no longer // approved, or ALL services if we are force rebinding diff --git a/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java new file mode 100644 index 000000000000..8ec716077f46 --- /dev/null +++ b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.os.instrumentation; + +import static android.Manifest.permission.DYNAMIC_INSTRUMENTATION; +import static android.content.Context.DYNAMIC_INSTRUMENTATION_SERVICE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.PermissionManuallyEnforced; +import android.content.Context; +import android.os.instrumentation.ExecutableMethodFileOffsets; +import android.os.instrumentation.IDynamicInstrumentationManager; +import android.os.instrumentation.MethodDescriptor; +import android.os.instrumentation.TargetProcess; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.SystemService; + +import dalvik.system.VMDebug; + +import java.lang.reflect.Method; + +/** + * System private implementation of the {@link IDynamicInstrumentationManager interface}. + */ +public class DynamicInstrumentationManagerService extends SystemService { + public DynamicInstrumentationManagerService(@NonNull Context context) { + super(context); + } + + @Override + public void onStart() { + publishBinderService(DYNAMIC_INSTRUMENTATION_SERVICE, new BinderService()); + } + + private final class BinderService extends IDynamicInstrumentationManager.Stub { + @Override + @PermissionManuallyEnforced + public @Nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets( + @NonNull TargetProcess targetProcess, @NonNull MethodDescriptor methodDescriptor) { + if (!com.android.art.flags.Flags.executableMethodFileOffsets()) { + throw new UnsupportedOperationException(); + } + getContext().enforceCallingOrSelfPermission( + DYNAMIC_INSTRUMENTATION, "Caller must have DYNAMIC_INSTRUMENTATION permission"); + + if (targetProcess.processName == null + || !targetProcess.processName.equals("system_server")) { + throw new UnsupportedOperationException( + "system_server is the only supported target process"); + } + + Method method = parseMethodDescriptor( + getClass().getClassLoader(), methodDescriptor); + VMDebug.ExecutableMethodFileOffsets location = + VMDebug.getExecutableMethodFileOffsets(method); + + if (location == null) { + return null; + } + + ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets(); + ret.containerPath = location.getContainerPath(); + ret.containerOffset = location.getContainerOffset(); + ret.methodOffset = location.getMethodOffset(); + return ret; + } + } + + @VisibleForTesting + static Method parseMethodDescriptor(ClassLoader classLoader, + @NonNull MethodDescriptor descriptor) { + try { + Class<?> javaClass = classLoader.loadClass(descriptor.fullyQualifiedClassName); + Class<?>[] parameters = new Class[descriptor.fullyQualifiedParameters.length]; + for (int i = 0; i < descriptor.fullyQualifiedParameters.length; i++) { + String typeName = descriptor.fullyQualifiedParameters[i]; + boolean isArrayType = typeName.endsWith("[]"); + if (isArrayType) { + typeName = typeName.substring(0, typeName.length() - 2); + } + switch (typeName) { + case "boolean": + parameters[i] = isArrayType ? boolean.class.arrayType() : boolean.class; + break; + case "byte": + parameters[i] = isArrayType ? byte.class.arrayType() : byte.class; + break; + case "char": + parameters[i] = isArrayType ? char.class.arrayType() : char.class; + break; + case "short": + parameters[i] = isArrayType ? short.class.arrayType() : short.class; + break; + case "int": + parameters[i] = isArrayType ? int.class.arrayType() : int.class; + break; + case "long": + parameters[i] = isArrayType ? long.class.arrayType() : long.class; + break; + case "float": + parameters[i] = isArrayType ? float.class.arrayType() : float.class; + break; + case "double": + parameters[i] = isArrayType ? double.class.arrayType() : double.class; + break; + default: + parameters[i] = isArrayType ? classLoader.loadClass(typeName).arrayType() + : classLoader.loadClass(typeName); + } + } + + return javaClass.getDeclaredMethod(descriptor.methodName, parameters); + } catch (ClassNotFoundException | NoSuchMethodException e) { + throw new IllegalArgumentException( + "The specified method cannot be found. Is this descriptor valid? " + + descriptor, e); + } + } +} diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index af2bb17fd0e6..d538bb876b64 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -265,6 +265,7 @@ public class BackgroundInstallControlService extends SystemService { @Override public void handleMessage(Message msg) { + Slog.d(TAG, "Package event received: " + msg.what); switch (msg.what) { case MSG_USAGE_EVENT_RECEIVED: mService.handleUsageEvent( @@ -326,6 +327,8 @@ public class BackgroundInstallControlService extends SystemService { return; } + Slog.d(TAG, "handlePackageAdd: adding " + packageName + " from " + + userId + " and notifying callbacks"); initBackgroundInstalledPackages(); mBackgroundInstalledPackages.add(userId, packageName); mCallbackHelper.notifyAllCallbacks(userId, packageName, INSTALL_EVENT_TYPE_INSTALL); @@ -364,7 +367,11 @@ public class BackgroundInstallControlService extends SystemService { // ADB sets installerPackageName to null, this creates a loophole to bypass BIC which will be // addressed with b/265203007 private boolean installedByAdb(String initiatingPackageName) { - return PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName); + if(PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName)) { + Slog.d(TAG, "handlePackageAdd: is installed by ADB, skipping"); + return true; + } + return false; } private boolean wasForegroundInstallation( @@ -407,6 +414,7 @@ public class BackgroundInstallControlService extends SystemService { if (mBackgroundInstalledPackages.contains(userId, packageName)) { mCallbackHelper.notifyAllCallbacks(userId, packageName, INSTALL_EVENT_TYPE_UNINSTALL); } + Slog.d(TAG, "handlePackageRemove: removing " + packageName + " from " + userId); mBackgroundInstalledPackages.remove(userId, packageName); writeBackgroundInstalledPackagesToDisk(); } diff --git a/services/core/java/com/android/server/pm/InstallDependencyHelper.java b/services/core/java/com/android/server/pm/InstallDependencyHelper.java new file mode 100644 index 000000000000..745665bab5b3 --- /dev/null +++ b/services/core/java/com/android/server/pm/InstallDependencyHelper.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SHARED_LIBRARY; + +import android.content.pm.SharedLibraryInfo; +import android.content.pm.parsing.PackageLite; +import android.os.OutcomeReceiver; + +import java.util.List; + +/** + * Helper class to interact with SDK Dependency Installer service. + */ +public class InstallDependencyHelper { + private final SharedLibrariesImpl mSharedLibraries; + + InstallDependencyHelper(SharedLibrariesImpl sharedLibraries) { + mSharedLibraries = sharedLibraries; + } + + void resolveLibraryDependenciesIfNeeded(PackageLite pkg, + OutcomeReceiver<Void, PackageManagerException> callback) { + final List<SharedLibraryInfo> missing; + try { + missing = mSharedLibraries.collectMissingSharedLibraryInfos(pkg); + } catch (PackageManagerException e) { + callback.onError(e); + return; + } + + if (missing.isEmpty()) { + // No need for dependency resolution. Move to installation directly. + callback.onResult(null); + return; + } + + try { + bindToDependencyInstaller(); + } catch (Exception e) { + PackageManagerException pe = new PackageManagerException( + INSTALL_FAILED_MISSING_SHARED_LIBRARY, e.getMessage()); + callback.onError(pe); + } + } + + private void bindToDependencyInstaller() { + throw new IllegalStateException("Failed to bind to Dependency Installer"); + } + + +} diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index ef0997696cd7..eb70748918b6 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -220,6 +220,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private AppOpsManager mAppOps; private final VerifierController mVerifierController; + private final InstallDependencyHelper mInstallDependencyHelper; private final HandlerThread mInstallThread; private final Handler mInstallHandler; @@ -346,6 +347,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements synchronized (mVerificationPolicyPerUser) { mVerificationPolicyPerUser.put(USER_SYSTEM, DEFAULT_VERIFICATION_POLICY); } + mInstallDependencyHelper = new InstallDependencyHelper( + mPm.mInjector.getSharedLibrariesImpl()); LocalServices.getService(SystemServiceManager.class).startService( new Lifecycle(context, this)); @@ -543,7 +546,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements session = PackageInstallerSession.readFromXml(in, mInternalCallback, mContext, mPm, mInstallThread.getLooper(), mStagingManager, mSessionsDir, this, mSilentUpdatePolicy, - mVerifierController); + mVerifierController, mInstallDependencyHelper); } catch (Exception e) { Slog.e(TAG, "Could not read session", e); continue; @@ -1065,7 +1068,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid, null, null, false, false, false, false, null, SessionInfo.INVALID_ID, false, false, false, PackageManager.INSTALL_UNKNOWN, "", null, - mVerifierController, verificationPolicy, verificationPolicy); + mVerifierController, verificationPolicy, verificationPolicy, + mInstallDependencyHelper); synchronized (mSessions) { mSessions.put(sessionId, session); diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 2a92de57446d..bad12016dca7 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -145,6 +145,7 @@ import android.os.FileUtils; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; import android.os.ParcelableException; import android.os.PersistableBundle; @@ -433,6 +434,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private final StagingManager mStagingManager; @NonNull private final VerifierController mVerifierController; + private final InstallDependencyHelper mInstallDependencyHelper; + final int sessionId; final int userId; final SessionParams params; @@ -1188,7 +1191,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { String sessionErrorMessage, DomainSet preVerifiedDomains, @NonNull VerifierController verifierController, @PackageInstaller.VerificationPolicy int initialVerificationPolicy, - @PackageInstaller.VerificationPolicy int currentVerificationPolicy) { + @PackageInstaller.VerificationPolicy int currentVerificationPolicy, + InstallDependencyHelper installDependencyHelper) { mCallback = callback; mContext = context; mPm = pm; @@ -1200,6 +1204,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mVerifierController = verifierController; mInitialVerificationPolicy = initialVerificationPolicy; mCurrentVerificationPolicy = new AtomicInteger(currentVerificationPolicy); + mInstallDependencyHelper = installDependencyHelper; this.sessionId = sessionId; this.userId = userId; @@ -2611,6 +2616,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { maybeFinishChildSessions(error, msg); } + private void onSessionDependencyResolveFailure(int error, String msg) { + Slog.e(TAG, "Failed to resolve dependency for session " + sessionId); + // Dispatch message to remove session from PackageInstallerService. + dispatchSessionFinished(error, msg, null); + maybeFinishChildSessions(error, msg); + } + private void onSystemDataLoaderUnrecoverable() { final String packageName = getPackageName(); if (TextUtils.isEmpty(packageName)) { @@ -3402,7 +3414,34 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { /* extras= */ null, /* forPreapproval= */ false); return; } - install(); + + if (Flags.sdkDependencyInstaller() && !isMultiPackage()) { + resolveLibraryDependenciesIfNeeded(); + } else { + install(); + } + } + + + private void resolveLibraryDependenciesIfNeeded() { + synchronized (mLock) { + // TODO(b/372862145): Callback should be called on a handler passed as parameter + mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(mPackageLite, + new OutcomeReceiver<>() { + + @Override + public void onResult(Void result) { + install(); + } + + @Override + public void onError(@NonNull PackageManagerException e) { + final String completeMsg = ExceptionUtils.getCompleteMessage(e); + setSessionFailed(e.error, completeMsg); + onSessionDependencyResolveFailure(e.error, completeMsg); + } + }); + } } /** @@ -6048,7 +6087,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @NonNull StagingManager stagingManager, @NonNull File sessionsDir, @NonNull PackageSessionProvider sessionProvider, @NonNull SilentUpdatePolicy silentUpdatePolicy, - @NonNull VerifierController verifierController) + @NonNull VerifierController verifierController, + @NonNull InstallDependencyHelper installDependencyHelper) throws IOException, XmlPullParserException { final int sessionId = in.getAttributeInt(null, ATTR_SESSION_ID); final int userId = in.getAttributeInt(null, ATTR_USER_ID); @@ -6257,6 +6297,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed, childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied, sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController, - initialVerificationPolicy, currentVerificationPolicy); + initialVerificationPolicy, currentVerificationPolicy, installDependencyHelper); } } diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java index a28e3c142220..52e8c52fe6af 100644 --- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java +++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java @@ -38,6 +38,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import java.util.ArrayList; @@ -45,7 +46,8 @@ import java.util.function.BiFunction; /** Helper class to handle PackageMonitorCallback and notify the registered client. This is mainly * used by PackageMonitor to improve the broadcast latency. */ -class PackageMonitorCallbackHelper { +@VisibleForTesting +public class PackageMonitorCallbackHelper { private static final boolean DEBUG = false; private static final String TAG = "PackageMonitorCallbackHelper"; diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java index 929fccce5265..fc54f6864db0 100644 --- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java +++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java @@ -33,6 +33,7 @@ import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.VersionedPackage; +import android.content.pm.parsing.PackageLite; import android.os.Build; import android.os.Process; import android.os.UserHandle; @@ -83,6 +84,7 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable private static final boolean DEBUG_SHARED_LIBRARIES = false; private static final String LIBRARY_TYPE_SDK = "sdk"; + private static final String LIBRARY_TYPE_STATIC = "static shared"; /** * Apps targeting Android S and above need to declare dependencies to the public native @@ -926,18 +928,19 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable if (!pkg.getUsesLibraries().isEmpty()) { usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesLibraries(), null, null, null, pkg.getPackageName(), "shared", true, pkg.getTargetSdkVersion(), null, - availablePackages, newLibraries); + availablePackages, newLibraries, null); } if (!pkg.getUsesStaticLibraries().isEmpty()) { usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesStaticLibraries(), pkg.getUsesStaticLibrariesVersions(), pkg.getUsesStaticLibrariesCertDigests(), - null, pkg.getPackageName(), "static shared", true, - pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, newLibraries); + null, pkg.getPackageName(), LIBRARY_TYPE_STATIC, true, + pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, newLibraries, + null); } if (!pkg.getUsesOptionalLibraries().isEmpty()) { usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesOptionalLibraries(), null, null, null, pkg.getPackageName(), "shared", false, pkg.getTargetSdkVersion(), - usesLibraryInfos, availablePackages, newLibraries); + usesLibraryInfos, availablePackages, newLibraries, null); } if (platformCompat.isChangeEnabledInternal(ENFORCE_NATIVE_SHARED_LIBRARY_DEPENDENCIES, pkg.getPackageName(), pkg.getTargetSdkVersion())) { @@ -945,13 +948,13 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesNativeLibraries(), null, null, null, pkg.getPackageName(), "native shared", true, pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, - newLibraries); + newLibraries, null); } if (!pkg.getUsesOptionalNativeLibraries().isEmpty()) { usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesOptionalNativeLibraries(), null, null, null, pkg.getPackageName(), "native shared", false, pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, - newLibraries); + newLibraries, null); } } if (!pkg.getUsesSdkLibraries().isEmpty()) { @@ -961,11 +964,34 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable pkg.getUsesSdkLibrariesVersionsMajor(), pkg.getUsesSdkLibrariesCertDigests(), pkg.getUsesSdkLibrariesOptional(), pkg.getPackageName(), LIBRARY_TYPE_SDK, required, pkg.getTargetSdkVersion(), - usesLibraryInfos, availablePackages, newLibraries); + usesLibraryInfos, availablePackages, newLibraries, null); } return usesLibraryInfos; } + List<SharedLibraryInfo> collectMissingSharedLibraryInfos(PackageLite pkgLite) + throws PackageManagerException { + ArrayList<SharedLibraryInfo> missingSharedLibrary = new ArrayList<>(); + synchronized (mPm.mLock) { + collectSharedLibraryInfos(pkgLite.getUsesSdkLibraries(), + pkgLite.getUsesSdkLibrariesVersionsMajor(), + pkgLite.getUsesSdkLibrariesCertDigests(), + /*libsOptional=*/ null, pkgLite.getPackageName(), LIBRARY_TYPE_SDK, + /*required=*/ true, pkgLite.getTargetSdk(), + /*outUsedLibraries=*/ null, mPm.mPackages, /*newLibraries=*/ null, + missingSharedLibrary); + + collectSharedLibraryInfos(pkgLite.getUsesStaticLibraries(), + pkgLite.getUsesStaticLibrariesVersions(), + pkgLite.getUsesStaticLibrariesCertDigests(), + /*libsOptional=*/ null, pkgLite.getPackageName(), LIBRARY_TYPE_STATIC, + /*required=*/ true, pkgLite.getTargetSdk(), + /*outUsedLibraries=*/ null, mPm.mPackages, /*newLibraries=*/ null, + missingSharedLibrary); + } + return missingSharedLibrary; + } + private ArrayList<SharedLibraryInfo> collectSharedLibraryInfos( @NonNull List<String> requestedLibraries, @Nullable long[] requiredVersions, @Nullable String[][] requiredCertDigests, @@ -973,7 +999,8 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable @NonNull String packageName, @NonNull String libraryType, boolean required, int targetSdk, @Nullable ArrayList<SharedLibraryInfo> outUsedLibraries, @NonNull final Map<String, AndroidPackage> availablePackages, - @Nullable final Map<String, WatchedLongSparseArray<SharedLibraryInfo>> newLibraries) + @Nullable final Map<String, WatchedLongSparseArray<SharedLibraryInfo>> newLibraries, + @Nullable final List<SharedLibraryInfo> outMissingSharedLibraryInfos) throws PackageManagerException { final int libCount = requestedLibraries.size(); for (int i = 0; i < libCount; i++) { @@ -986,16 +1013,33 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable libName, libVersion, mSharedLibraries, newLibraries); } if (libraryInfo == null) { - // Only allow app be installed if the app specifies the sdk-library dependency is - // optional - if (required || (LIBRARY_TYPE_SDK.equals(libraryType) && (libsOptional != null - && !libsOptional[i]))) { - throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY, - "Package " + packageName + " requires unavailable " + libraryType - + " library " + libName + "; failing!"); - } else if (DEBUG_SHARED_LIBRARIES) { - Slog.i(TAG, "Package " + packageName + " desires unavailable " + libraryType - + " library " + libName + "; ignoring!"); + if (required) { + boolean isSdkOrStatic = libraryType.equals(LIBRARY_TYPE_SDK) + || libraryType.equals(LIBRARY_TYPE_STATIC); + if (isSdkOrStatic && outMissingSharedLibraryInfos != null) { + // TODO(b/372862145): Pass the CertDigest too + // If Dependency Installation is supported, try that instead of failing. + SharedLibraryInfo missingLibrary = new SharedLibraryInfo( + libName, libVersion, SharedLibraryInfo.TYPE_SDK_PACKAGE + ); + outMissingSharedLibraryInfos.add(missingLibrary); + } else { + throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY, + "Package " + packageName + " requires unavailable " + libraryType + + " library " + libName + "; failing!"); + } + } else { + // Only allow app be installed if the app specifies the sdk-library + // dependency is optional + boolean isOptional = libsOptional != null && libsOptional[i]; + if (LIBRARY_TYPE_SDK.equals(libraryType) && !isOptional) { + throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY, + "Package " + packageName + " requires unavailable " + libraryType + + " library " + libName + "; failing!"); + } else if (DEBUG_SHARED_LIBRARIES) { + Slog.i(TAG, "Package " + packageName + " desires unavailable " + libraryType + + " library " + libName + "; ignoring!"); + } } } else { if (requiredVersions != null && requiredCertDigests != null) { diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 19406b46f5c0..8d039f19db8c 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -83,7 +83,7 @@ import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED; import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled; -import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable; +import static com.android.hardware.input.Flags.enableNew25q2Keycodes; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; @@ -3993,10 +3993,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } case KeyEvent.KEYCODE_SCREENSHOT: - if (emojiAndScreenshotKeycodesAvailable() && down && repeatCount == 0) { + if (firstDown) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); } return true; + case KeyEvent.KEYCODE_DO_NOT_DISTURB: + case KeyEvent.KEYCODE_LOCK: + case KeyEvent.KEYCODE_FULLSCREEN: + return true; } if (isValidGlobalKey(keyCode) && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) { @@ -5666,9 +5670,23 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_MACRO_4: result &= ~ACTION_PASS_TO_USER; break; - case KeyEvent.KEYCODE_EMOJI_PICKER: - if (!emojiAndScreenshotKeycodesAvailable()) { - // Don't allow EMOJI_PICKER key to be dispatched until flag is released. + case KeyEvent.KEYCODE_DICTATE: + case KeyEvent.KEYCODE_NEW: + case KeyEvent.KEYCODE_CLOSE: + case KeyEvent.KEYCODE_PRINT: + case KeyEvent.KEYCODE_F13: + case KeyEvent.KEYCODE_F14: + case KeyEvent.KEYCODE_F15: + case KeyEvent.KEYCODE_F16: + case KeyEvent.KEYCODE_F17: + case KeyEvent.KEYCODE_F18: + case KeyEvent.KEYCODE_F19: + case KeyEvent.KEYCODE_F20: + case KeyEvent.KEYCODE_F21: + case KeyEvent.KEYCODE_F22: + case KeyEvent.KEYCODE_F23: + case KeyEvent.KEYCODE_F24: + if (!enableNew25q2Keycodes()) { result &= ~ACTION_PASS_TO_USER; } break; @@ -5841,8 +5859,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action, long whenNanos, int policyFlags) { if ((policyFlags & FLAG_WAKE) != 0) { - if (mWindowWakeUpPolicy.wakeUpFromMotion( - whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) { + if (mWindowWakeUpPolicy.wakeUpFromMotion(displayId, whenNanos / 1000000, source, + action == MotionEvent.ACTION_DOWN)) { // Woke up. Pass motion events to user. return ACTION_PASS_TO_USER; } @@ -5856,8 +5874,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { // there will be no dream to intercept the touch and wake into ambient. The device should // wake up in this case. if (isTheaterModeEnabled() && (policyFlags & FLAG_WAKE) != 0) { - if (mWindowWakeUpPolicy.wakeUpFromMotion( - whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) { + if (mWindowWakeUpPolicy.wakeUpFromMotion(displayId, whenNanos / 1000000, source, + action == MotionEvent.ACTION_DOWN)) { // Woke up. Pass motion events to user. return ACTION_PASS_TO_USER; } @@ -6205,7 +6223,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } private void wakeUpFromWakeKey(long eventTime, int keyCode, boolean isDown) { - if (mWindowWakeUpPolicy.wakeUpFromKey(eventTime, keyCode, isDown)) { + if (mWindowWakeUpPolicy.wakeUpFromKey(DEFAULT_DISPLAY, eventTime, keyCode, isDown)) { final boolean keyCanLaunchHome = keyCode == KEYCODE_HOME || keyCode == KEYCODE_POWER; // Start HOME with "reason" extra if sleeping for more than mWakeUpToLastStateTimeout if (shouldWakeUpWithHomeIntent() && keyCanLaunchHome) { diff --git a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java index af1ad13f1d15..04dbd1fea5d6 100644 --- a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java +++ b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java @@ -25,6 +25,7 @@ import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION; import static android.view.KeyEvent.KEYCODE_POWER; import static com.android.server.policy.Flags.supportInputWakeupDelegate; +import static com.android.server.power.feature.flags.Flags.perDisplayWakeByTouch; import android.annotation.Nullable; import android.content.Context; @@ -107,13 +108,14 @@ class WindowWakeUpPolicy { /** * Wakes up from a key event. * + * @param displayId the id of the display to wake. * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}. * @param keyCode the {@link android.view.KeyEvent} key code of the key event. * @param isDown {@code true} if the event's action is {@link KeyEvent#ACTION_DOWN}. * @return {@code true} if the policy allows the requested wake up and the request has been * executed; {@code false} otherwise. */ - boolean wakeUpFromKey(long eventTime, int keyCode, boolean isDown) { + boolean wakeUpFromKey(int displayId, long eventTime, int keyCode, boolean isDown) { final boolean wakeAllowedDuringTheaterMode = keyCode == KEYCODE_POWER ? mAllowTheaterModeWakeFromPowerKey @@ -126,22 +128,31 @@ class WindowWakeUpPolicy { && mInputWakeUpDelegate.wakeUpFromKey(eventTime, keyCode, isDown)) { return true; } - wakeUp( - eventTime, - keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY, - keyCode == KEYCODE_POWER ? "POWER" : "KEY"); + if (perDisplayWakeByTouch()) { + wakeUp( + displayId, + eventTime, + keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY, + keyCode == KEYCODE_POWER ? "POWER" : "KEY"); + } else { + wakeUp( + eventTime, + keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY, + keyCode == KEYCODE_POWER ? "POWER" : "KEY"); + } return true; } /** * Wakes up from a motion event. * + * @param displayId the id of the display to wake. * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}. * @param isDown {@code true} if the event's action is {@link MotionEvent#ACTION_DOWN}. * @return {@code true} if the policy allows the requested wake up and the request has been * executed; {@code false} otherwise. */ - boolean wakeUpFromMotion(long eventTime, int source, boolean isDown) { + boolean wakeUpFromMotion(int displayId, long eventTime, int source, boolean isDown) { if (!canWakeUp(mAllowTheaterModeWakeFromMotion)) { if (DEBUG) Slog.d(TAG, "Unable to wake up from motion."); return false; @@ -150,7 +161,11 @@ class WindowWakeUpPolicy { && mInputWakeUpDelegate.wakeUpFromMotion(eventTime, source, isDown)) { return true; } - wakeUp(eventTime, WAKE_REASON_WAKE_MOTION, "MOTION"); + if (perDisplayWakeByTouch()) { + wakeUp(displayId, eventTime, WAKE_REASON_WAKE_MOTION, "MOTION"); + } else { + wakeUp(eventTime, WAKE_REASON_WAKE_MOTION, "MOTION"); + } return true; } @@ -237,4 +252,12 @@ class WindowWakeUpPolicy { private void wakeUp(long wakeTime, @WakeReason int reason, String details) { mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details); } + + /** Wakes up given display. */ + private void wakeUp(int displayId, long wakeTime, @WakeReason int reason, String details) { + // If we're given an invalid display id to wake, fall back to waking default display + final int displayIdToWake = + displayId == Display.INVALID_DISPLAY ? Display.DEFAULT_DISPLAY : displayId; + mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details, displayIdToWake); + } } diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 677a2dea665b..028ac57fc5a3 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -303,6 +303,8 @@ public class BatteryStatsImpl extends BatteryStats { private final GnssPowerStatsCollector mGnssPowerStatsCollector; private final CustomEnergyConsumerPowerStatsCollector mCustomEnergyConsumerPowerStatsCollector; private final SparseBooleanArray mPowerStatsCollectorEnabled = new SparseBooleanArray(); + private boolean mMoveWscLoggingToNotifierEnabled = false; + private ScreenPowerStatsCollector.ScreenUsageTimeRetriever mScreenUsageTimeRetriever = new ScreenPowerStatsCollector.ScreenUsageTimeRetriever() { @@ -5155,7 +5157,7 @@ public class BatteryStatsImpl extends BatteryStats { Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs); uidStats.noteStartWakeLocked(pid, name, type, elapsedRealtimeMs); - if (!mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled()) { + if (!mMoveWscLoggingToNotifierEnabled) { mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name, uidStats.mProcessState, true /* acquired */, getPowerManagerWakeLockLevel(type)); @@ -5206,7 +5208,7 @@ public class BatteryStatsImpl extends BatteryStats { Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs); uidStats.noteStopWakeLocked(pid, name, type, elapsedRealtimeMs); - if (!mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled()) { + if (!mMoveWscLoggingToNotifierEnabled) { mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name, uidStats.mProcessState, false/* acquired */, getPowerManagerWakeLockLevel(type)); @@ -15975,6 +15977,15 @@ public class BatteryStatsImpl extends BatteryStats { } } + /** + * Controls where the logging of the WakelockStateChanged atom occurs: + * true = Notifier, false = BatteryStatsImpl. + */ + public void setMoveWscLoggingToNotifierEnabled(boolean enabled) { + synchronized (this) { + mMoveWscLoggingToNotifierEnabled = enabled; + } + } @GuardedBy("this") public void systemServicesReady(Context context) { mConstants.startObserving(context.getContentResolver()); diff --git a/services/core/java/com/android/server/adaptiveauth/AdaptiveAuthService.java b/services/core/java/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationService.java index 9398c7a854d4..b129fdc1b6e3 100644 --- a/services/core/java/com/android/server/adaptiveauth/AdaptiveAuthService.java +++ b/services/core/java/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.adaptiveauth; +package com.android.server.security.adaptiveauthentication; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST; @@ -55,8 +55,8 @@ import java.util.Objects; /** * @hide */ -public class AdaptiveAuthService extends SystemService { - private static final String TAG = "AdaptiveAuthService"; +public class AdaptiveAuthenticationService extends SystemService { + private static final String TAG = "AdaptiveAuthenticationService"; private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG); @VisibleForTesting @@ -78,12 +78,12 @@ public class AdaptiveAuthService extends SystemService { final SparseIntArray mFailedAttemptsForUser = new SparseIntArray(); private final SparseLongArray mLastLockedTimestamp = new SparseLongArray(); - public AdaptiveAuthService(Context context) { + public AdaptiveAuthenticationService(Context context) { this(context, new LockPatternUtils(context)); } @VisibleForTesting - public AdaptiveAuthService(Context context, LockPatternUtils lockPatternUtils) { + public AdaptiveAuthenticationService(Context context, LockPatternUtils lockPatternUtils) { super(context); mLockPatternUtils = lockPatternUtils; mLockSettings = Objects.requireNonNull( diff --git a/services/core/java/com/android/server/security/adaptiveauthentication/OWNERS b/services/core/java/com/android/server/security/adaptiveauthentication/OWNERS new file mode 100644 index 000000000000..29affcdb81aa --- /dev/null +++ b/services/core/java/com/android/server/security/adaptiveauthentication/OWNERS @@ -0,0 +1,3 @@ +hainingc@google.com +jbolinger@google.com +graciecheng@google.com diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 10f096c9031b..e29f05336760 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -2410,6 +2410,27 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } @Override + public Bundle getCurrentBitmapCrops(int which, int userId) { + userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), + Binder.getCallingUid(), userId, false, true, "getBitmapCrop", null); + synchronized (mLock) { + checkPermission(READ_WALLPAPER_INTERNAL); + WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId) + : mWallpaperMap.get(userId); + if (wallpaper == null || !mImageWallpaper.equals(wallpaper.getComponent())) { + return null; + } + Bundle bundle = new Bundle(); + for (int i = 0; i < wallpaper.mCropHints.size(); i++) { + String key = String.valueOf(wallpaper.mCropHints.keyAt(i)); + Rect rect = wallpaper.mCropHints.valueAt(i); + bundle.putParcelable(key, rect); + } + return bundle; + } + } + + @Override public List<Rect> getFutureBitmapCrops(Point bitmapSize, List<Point> displaySizes, int[] screenOrientations, List<Rect> crops) { SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 73ae51c6e64a..14be59f27f84 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -254,6 +254,7 @@ import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; import static org.xmlpull.v1.XmlPullParser.START_TAG; +import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -10308,6 +10309,21 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (pictureInPictureArgs != null && pictureInPictureArgs.hasSourceBoundsHint()) { pictureInPictureArgs.getSourceRectHint().offset(windowBounds.left, windowBounds.top); } + + if (android.app.Flags.enableTvImplicitEnterPipRestriction()) { + PackageManager pm = mAtmService.mContext.getPackageManager(); + if (pictureInPictureArgs.isAutoEnterEnabled() + && pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + && pm.checkPermission(Manifest.permission.TV_IMPLICIT_ENTER_PIP, packageName) + == PackageManager.PERMISSION_DENIED) { + Log.i(TAG, + "Auto-enter PiP only allowed on TV if android.permission" + + ".TV_IMPLICIT_ENTER_PIP permission is held by the app."); + PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder(); + builder.setAutoEnterEnabled(false); + pictureInPictureArgs.copyOnlySet(builder.build()); + } + } } private void applyLocaleOverrideIfNeeded(Configuration resolvedConfig) { diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index ec171c5e5766..a71620d66f80 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -46,10 +46,8 @@ import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; import static com.android.window.flags.Flags.balAdditionalStartModes; import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; -import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck; import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; -import static com.android.window.flags.Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid; import static com.android.window.flags.Flags.balShowToastsBlocked; import static com.android.window.flags.Flags.balStrictModeRo; @@ -348,11 +346,7 @@ public class BackgroundActivityStartController { @BackgroundActivityStartMode int realCallerBackgroundActivityStartMode = checkedOptions.getPendingIntentBackgroundActivityStartMode(); - if (!balImproveRealCallerVisibilityCheck()) { - // without this fix the auto-opt ins below would violate CTS tests - mAutoOptInReason = null; - mAutoOptInCaller = false; - } else if (originatingPendingIntent == null) { + if (originatingPendingIntent == null) { mAutoOptInReason = AUTO_OPT_IN_NOT_PENDING_INTENT; mAutoOptInCaller = true; } else if (mIsCallForResult) { @@ -599,12 +593,8 @@ public class BackgroundActivityStartController { mCheckedOptions.getPendingIntentBackgroundActivityStartMode())); } // features - sb.append("; balImproveRealCallerVisibilityCheck: ") - .append(balImproveRealCallerVisibilityCheck()); sb.append("; balRequireOptInByPendingIntentCreator: ") .append(balRequireOptInByPendingIntentCreator()); - sb.append("; balRespectAppSwitchStateWhenCheckBoundByForegroundUid: ") - .append(balRespectAppSwitchStateWhenCheckBoundByForegroundUid()); sb.append("; balDontBringExistingBackgroundTaskStackToFg: ") .append(balDontBringExistingBackgroundTaskStackToFg()); sb.append("]"); @@ -1133,23 +1123,13 @@ public class BackgroundActivityStartController { final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW || state.mAppSwitchState == APP_SWITCH_FG_ONLY || isHomeApp(state.mRealCallingUid, state.mRealCallingPackage); - if (balImproveRealCallerVisibilityCheck()) { - if (appSwitchAllowedOrFg && state.mRealCallingUidHasAnyVisibleWindow) { - return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, - /*background*/ false, "realCallingUid has visible window"); - } - if (mService.mActiveUids.hasNonAppVisibleWindow(state.mRealCallingUid)) { - return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW, - /*background*/ false, "realCallingUid has non-app visible window"); - } - } else { - // don't abort if the realCallingUid has a visible window - // TODO(b/171459802): We should check appSwitchAllowed also - if (state.mRealCallingUidHasAnyVisibleWindow) { - return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, - /*background*/ false, - "realCallingUid has visible (non-toast) window."); - } + if (appSwitchAllowedOrFg && state.mRealCallingUidHasAnyVisibleWindow) { + return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, + /*background*/ false, "realCallingUid has visible window"); + } + if (mService.mActiveUids.hasNonAppVisibleWindow(state.mRealCallingUid)) { + return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW, + /*background*/ false, "realCallingUid has non-app visible window"); } // Don't abort if the realCallerApp or other processes of that uid are considered to be in diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java index 264c8beb44bf..ccf1aedb3177 100644 --- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java +++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java @@ -50,7 +50,6 @@ import android.util.IntArray; import com.android.internal.annotations.GuardedBy; import com.android.server.wm.BackgroundActivityStartController.BalVerdict; -import com.android.window.flags.Flags; import java.io.PrintWriter; import java.util.ArrayList; @@ -137,10 +136,8 @@ class BackgroundLaunchProcessController { } // Allow if the caller is bound by a UID that's currently foreground. // But still respect the appSwitchState. - if (checkConfiguration.checkVisibility && ( - Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid() - ? appSwitchState != APP_SWITCH_DISALLOW && isBoundByForegroundUid() - : isBoundByForegroundUid())) { + if (checkConfiguration.checkVisibility && appSwitchState != APP_SWITCH_DISALLOW + && isBoundByForegroundUid()) { return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_BOUND_BY_FOREGROUND : BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false, "process bound by foreground uid"); diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index 3f6e91590cce..9a48d5b8880d 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -18,7 +18,6 @@ package com.android.server.wm; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; import static com.android.server.wm.SurfaceAnimatorProto.ANIMATION_ADAPTER; -import static com.android.server.wm.SurfaceAnimatorProto.ANIMATION_START_DELAYED; import static com.android.server.wm.SurfaceAnimatorProto.LEASH; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -90,8 +89,6 @@ public class SurfaceAnimator { @Nullable private Runnable mAnimationCancelledCallback; - private boolean mAnimationStartDelayed; - private boolean mAnimationFinished; /** @@ -188,10 +185,6 @@ public class SurfaceAnimator { mAnimatable.onAnimationLeashCreated(t, mLeash); } mAnimatable.onLeashAnimationStarting(t, mLeash); - if (mAnimationStartDelayed) { - ProtoLog.i(WM_DEBUG_ANIM, "Animation start delayed for %s", mAnimatable); - return; - } mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback); if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG)) { StringWriter sw = new StringWriter(); @@ -215,36 +208,7 @@ public class SurfaceAnimator { null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */); } - /** - * Begins with delaying all animations to start. Any subsequent call to {@link #startAnimation} - * will not start the animation until {@link #endDelayingAnimationStart} is called. When an - * animation start is being delayed, the animator is considered animating already. - */ - void startDelayingAnimationStart() { - - // We only allow delaying animation start we are not currently animating - if (!isAnimating()) { - mAnimationStartDelayed = true; - } - } - - /** - * See {@link #startDelayingAnimationStart}. - */ - void endDelayingAnimationStart() { - final boolean delayed = mAnimationStartDelayed; - mAnimationStartDelayed = false; - if (delayed && mAnimation != null) { - mAnimation.startAnimation(mLeash, mAnimatable.getSyncTransaction(), - mAnimationType, mInnerAnimationFinishedCallback); - mAnimatable.commitPendingTransaction(); - } - } - - /** - * @return Whether we are currently running an animation, or we have a pending animation that - * is waiting to be started with {@link #endDelayingAnimationStart} - */ + /** Returns whether it is currently running an animation. */ boolean isAnimating() { return mAnimation != null; } @@ -290,15 +254,6 @@ public class SurfaceAnimator { } /** - * Reparents the surface. - * - * @see #setLayer - */ - void reparent(Transaction t, SurfaceControl newParent) { - t.reparent(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), newParent); - } - - /** * @return True if the surface is attached to the leash; false otherwise. */ boolean hasLeash() { @@ -319,7 +274,6 @@ public class SurfaceAnimator { Slog.w(TAG, "Unable to transfer animation, because " + from + " animation is finished"); return; } - endDelayingAnimationStart(); final Transaction t = mAnimatable.getSyncTransaction(); cancelAnimation(t, true /* restarting */, true /* forwardCancel */); mLeash = from.mLeash; @@ -336,10 +290,6 @@ public class SurfaceAnimator { mService.mAnimationTransferMap.put(mAnimation, this); } - boolean isAnimationStartDelayed() { - return mAnimationStartDelayed; - } - /** * Cancels the animation, and resets the leash. * @@ -361,7 +311,7 @@ public class SurfaceAnimator { final SurfaceFreezer.Snapshot snapshot = mSnapshot; reset(t, false); if (animation != null) { - if (!mAnimationStartDelayed && forwardCancel) { + if (forwardCancel) { animation.onAnimationCancelled(leash); if (animationCancelledCallback != null) { animationCancelledCallback.run(); @@ -386,10 +336,6 @@ public class SurfaceAnimator { mService.scheduleAnimationLocked(); } } - - if (!restarting) { - mAnimationStartDelayed = false; - } } private void reset(Transaction t, boolean destroyLeash) { @@ -495,14 +441,12 @@ public class SurfaceAnimator { if (mLeash != null) { mLeash.dumpDebug(proto, LEASH); } - proto.write(ANIMATION_START_DELAYED, mAnimationStartDelayed); proto.end(token); } void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("mLeash="); pw.print(mLeash); - pw.print(" mAnimationType=" + animationTypeToString(mAnimationType)); - pw.println(mAnimationStartDelayed ? " mAnimationStartDelayed=true" : ""); + pw.print(" mAnimationType="); pw.println(animationTypeToString(mAnimationType)); pw.print(prefix); pw.print("Animation: "); pw.println(mAnimation); if (mAnimation != null) { mAnimation.dump(pw, prefix + " "); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a6034664af5a..20481f25fa5c 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -465,6 +465,31 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return false; } + /** + * This ensures that all changes for previously transient-hide containers are flagged such that + * they will report changes and be included in this transition. + */ + void updateChangesForRestoreTransientHideTasks(Transition transientLaunchTransition) { + if (transientLaunchTransition.mTransientHideTasks == null) { + // Skip if the transient-launch transition has no transient-hide tasks + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Skipping update changes for restore transient hide tasks"); + return; + } + + // For each change, if it was previously transient-hidden, then we should force a flag to + // ensure that it is included in the next transition + for (int i = 0; i < mChanges.size(); i++) { + final WindowContainer container = mChanges.keyAt(i); + if (transientLaunchTransition.isInTransientHide(container)) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Force update transient hide task for restore %d: %s", mSyncId, container); + final ChangeInfo info = mChanges.valueAt(i); + info.mRestoringTransientHide = true; + } + } + } + /** Returns {@code true} if the task should keep visible if this is a transient transition. */ boolean isTransientVisible(@NonNull Task task) { if (mTransientLaunches == null) return false; @@ -3478,6 +3503,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // State tracking boolean mExistenceChanged = false; + // This state indicates that we are restoring transient order as a part of an + // end-transition. Because the visibility for transient hide containers has not actually + // changed, we need to ensure that hasChanged() still reports the relevant changes + boolean mRestoringTransientHide = false; // before change state boolean mVisible; int mWindowingMode; @@ -3552,7 +3581,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { || !mContainer.getBounds().equals(mAbsoluteBounds) || mRotation != mContainer.getWindowConfiguration().getRotation() || mDisplayId != getDisplayId(mContainer) - || (mFlags & ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP) != 0; + || (mFlags & ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP) != 0 + // If we are restoring transient-hide containers, then we should consider them + // important for the transition as well (their requested visibilities would not + // have changed for the checks below to consider it). + || mRestoringTransientHide; } @TransitionInfo.TransitionMode @@ -3565,6 +3598,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } final boolean nowVisible = wc.isVisibleRequested(); if (nowVisible == mVisible) { + if (mRestoringTransientHide) { + // The requested visibility has not changed for transient-hide containers, but + // we are restoring them so we should considering them moving to front again + return TRANSIT_TO_FRONT; + } return TRANSIT_CHANGE; } if (mExistenceChanged) { diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 87bdfa4f5d75..143d1b72fff9 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -37,6 +37,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.util.ArrayMap; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; @@ -524,6 +525,23 @@ class TransitionController { return false; } + /** + * @return A pair of the transition and restore-behind target for the given {@param container}. + * @param container An ancestor of a transient-launch activity + */ + @Nullable + Pair<Transition, Task> getTransientLaunchTransitionAndTarget( + @NonNull WindowContainer container) { + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + final Transition transition = mPlayingTransitions.get(i); + final Task restoreBehindTask = transition.getTransientLaunchRestoreTarget(container); + if (restoreBehindTask != null) { + return new Pair<>(transition, restoreBehindTask); + } + } + return null; + } + /** Returns {@code true} if the display contains a transient-launch transition. */ boolean hasTransientLaunch(@NonNull DisplayContent dc) { if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch() diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index e0c473de0f33..5f92bb626154 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3215,8 +3215,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< final boolean isChanging = AppTransition.isChangeTransitOld(transit) && enter && isChangingAppTransition(); - // Delaying animation start isn't compatible with remote animations at all. - if (controller != null && !mSurfaceAnimator.isAnimationStartDelayed()) { + if (controller != null) { // Here we load App XML in order to read com.android.R.styleable#Animation_showBackdrop. boolean showBackdrop = false; // Optionally set backdrop color if App explicitly provides it through @@ -3639,20 +3638,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return getAnimatingContainer(PARENTS, ANIMATION_TYPE_ALL); } - /** - * @see SurfaceAnimator#startDelayingAnimationStart - */ - void startDelayingAnimationStart() { - mSurfaceAnimator.startDelayingAnimationStart(); - } - - /** - * @see SurfaceAnimator#endDelayingAnimationStart - */ - void endDelayingAnimationStart() { - mSurfaceAnimator.endDelayingAnimationStart(); - } - @Override public int getSurfaceWidth() { return mSurfaceControl.getWidth(); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index dac8f69a4cae..ead12826c263 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -111,6 +111,7 @@ import android.os.RemoteException; import android.util.AndroidRuntimeException; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Pair; import android.util.Slog; import android.view.RemoteAnimationAdapter; import android.view.SurfaceControl; @@ -1375,16 +1376,56 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } case HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER: { - if (!chain.isFinishing()) break; + if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + // Only allow restoring transient order when finishing a transition + if (!chain.isFinishing()) break; + } + // Validate the container final WindowContainer container = WindowContainer.fromBinder(hop.getContainer()); - if (container == null) break; + if (container == null) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Restoring transient order: invalid container"); + break; + } final Task thisTask = container.asActivityRecord() != null ? container.asActivityRecord().getTask() : container.asTask(); - if (thisTask == null) break; - final Task restoreAt = chain.mTransition.getTransientLaunchRestoreTarget(container); - if (restoreAt == null) break; + if (thisTask == null) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Restoring transient order: invalid task"); + break; + } + + // Find the task to restore behind + final Pair<Transition, Task> transientRestore = + mTransitionController.getTransientLaunchTransitionAndTarget(container); + if (transientRestore == null) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Restoring transient order: no restore task"); + break; + } + final Transition transientLaunchTransition = transientRestore.first; + final Task restoreAt = transientRestore.second; + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Restoring transient order: restoring behind task=%d", restoreAt.mTaskId); + + // Restore the position of the given container behind the target task final TaskDisplayArea taskDisplayArea = thisTask.getTaskDisplayArea(); taskDisplayArea.moveRootTaskBehindRootTask(thisTask.getRootTask(), restoreAt); + + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + // Because we are in a transient launch transition, the requested visibility of + // tasks does not actually change for the transient-hide tasks, but we do want + // the restoration of these transient-hide tasks to top to be a part of this + // finish transition + final Transition collectingTransition = + mTransitionController.getCollectingTransition(); + if (collectingTransition != null) { + collectingTransition.updateChangesForRestoreTransientHideTasks( + transientLaunchTransition); + } + } + + effects |= TRANSACT_EFFECTS_LIFECYCLE; break; } case HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER: { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 3805c02d1bb9..19b03437292f 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -119,7 +119,6 @@ import com.android.internal.widget.ILockSettings; import com.android.internal.widget.LockSettingsInternal; import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.accounts.AccountManagerService; -import com.android.server.adaptiveauth.AdaptiveAuthService; import com.android.server.adb.AdbService; import com.android.server.alarm.AlarmManagerService; import com.android.server.am.ActivityManagerService; @@ -205,6 +204,7 @@ import com.android.server.os.BugreportManagerService; import com.android.server.os.DeviceIdentifiersPolicyService; import com.android.server.os.NativeTombstoneManagerService; import com.android.server.os.SchedulingPolicyService; +import com.android.server.os.instrumentation.DynamicInstrumentationManagerService; import com.android.server.pdb.PersistentDataBlockService; import com.android.server.people.PeopleService; import com.android.server.permission.access.AccessCheckingService; @@ -249,6 +249,7 @@ import com.android.server.security.AttestationVerificationManagerService; import com.android.server.security.FileIntegrityService; import com.android.server.security.KeyAttestationApplicationIdProviderService; import com.android.server.security.KeyChainSystemService; +import com.android.server.security.adaptiveauthentication.AdaptiveAuthenticationService; import com.android.server.security.advancedprotection.AdvancedProtectionService; import com.android.server.security.rkp.RemoteProvisioningService; import com.android.server.selinux.SelinuxAuditLogsService; @@ -1620,7 +1621,8 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(ROLE_SERVICE_CLASS); t.traceEnd(); - if (!isWatch && android.app.supervision.flags.Flags.supervisionApi()) { + if (android.app.supervision.flags.Flags.supervisionApi() + && (!isWatch || android.app.supervision.flags.Flags.supervisionApiOnWear())) { t.traceBegin("StartSupervisionService"); mSystemServiceManager.startService(SupervisionService.Lifecycle.class); t.traceEnd(); @@ -2650,8 +2652,8 @@ public final class SystemServer implements Dumpable { t.traceEnd(); if (android.adaptiveauth.Flags.enableAdaptiveAuth()) { - t.traceBegin("StartAdaptiveAuthService"); - mSystemServiceManager.startService(AdaptiveAuthService.class); + t.traceBegin("StartAdaptiveAuthenticationService"); + mSystemServiceManager.startService(AdaptiveAuthenticationService.class); t.traceEnd(); } @@ -2890,6 +2892,13 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(TracingServiceProxy.class); t.traceEnd(); + // UprobeStats DynamicInstrumentationManager + if (com.android.art.flags.Flags.executableMethodFileOffsets()) { + t.traceBegin("StartDynamicInstrumentationManager"); + mSystemServiceManager.startService(DynamicInstrumentationManagerService.class); + t.traceEnd(); + } + // It is now time to start up the app processes... t.traceBegin("MakeLockSettingsServiceReady"); diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/Android.bp b/services/tests/DynamicInstrumentationManagerServiceTests/Android.bp new file mode 100644 index 000000000000..2c2e5fdb68d9 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/Android.bp @@ -0,0 +1,44 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_system_performance", +} + +android_test { + name: "DynamicInstrumentationManagerServiceTests", + srcs: ["src/**/*.java"], + + static_libs: [ + "androidx.test.core", + "androidx.test.runner", + "hamcrest-library", + "platform-test-annotations", + "services.core", + "testables", + "truth", + ], + + certificate: "platform", + platform_apis: true, + test_suites: [ + "device-tests", + ], +} diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/AndroidManifest.xml b/services/tests/DynamicInstrumentationManagerServiceTests/AndroidManifest.xml new file mode 100644 index 000000000000..4913d706a72d --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.os.instrumentation" > + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.os.instrumentation" + android:label="DynamicInstrumentationmanagerService Unit Tests"/> +</manifest>
\ No newline at end of file diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/TEST_MAPPING b/services/tests/DynamicInstrumentationManagerServiceTests/TEST_MAPPING new file mode 100644 index 000000000000..33defed0eae6 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "DynamicInstrumentationManagerServiceTests" + } + ] +} diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClass.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClass.java new file mode 100644 index 000000000000..04073fab2059 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClass.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +public abstract class TestAbstractClass { + abstract void abstractMethod(); + + void concreteMethod() { + } +} diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClassImpl.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClassImpl.java new file mode 100644 index 000000000000..2c25e7a52f73 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClassImpl.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +public class TestAbstractClassImpl extends TestAbstractClass { + @Override + void abstractMethod() { + } +} diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestClass.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestClass.java new file mode 100644 index 000000000000..085f5953f0e5 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestClass.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +public class TestClass { + void primitiveParams(boolean a, boolean[] b, byte c, byte[] d, char e, char[] f, short g, + short[] h, int i, int[] j, long k, long[] l, float m, float[] n, double o, double[] p) { + } + + void classParams(String a, String[] b) { + } + + private void privateMethod() { + } + + /** + * docs! + */ + public void publicMethod() { + } + + private static class InnerClass { + private void innerMethod(InnerClass arg, InnerClass[] argArray) { + } + } +} diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterface.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterface.java new file mode 100644 index 000000000000..7af4f254ab1c --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterface.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +/** + * docs! + */ +public interface TestInterface { + /** + * docs! + */ + void interfaceMethod(); + + /** + * docs! + */ + default void defaultMethod() { + } +} diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterfaceImpl.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterfaceImpl.java new file mode 100644 index 000000000000..53aecbc08939 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterfaceImpl.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +public class TestInterfaceImpl implements TestInterface { + @Override + public void interfaceMethod() { + } +} diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java new file mode 100644 index 000000000000..5492ba6b9dd1 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.os.instrumentation; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import android.os.instrumentation.MethodDescriptor; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import com.android.server.TestAbstractClass; +import com.android.server.TestAbstractClassImpl; +import com.android.server.TestClass; +import com.android.server.TestInterface; +import com.android.server.TestInterfaceImpl; + +import org.junit.Test; + +import java.lang.reflect.Method; + + +/** + * Test class for + * {@link DynamicInstrumentationManagerService#parseMethodDescriptor(ClassLoader, + * MethodDescriptor)}. + * <p> + * Build/Install/Run: + * atest FrameworksMockingServicesTests:ParseMethodDescriptorTest + */ +@Presubmit +@SmallTest +public class ParseMethodDescriptorTest { + private static final String[] PRIMITIVE_PARAMS = new String[]{ + "boolean", "boolean[]", "byte", "byte[]", "char", "char[]", "short", "short[]", "int", + "int[]", "long", "long[]", "float", "float[]", "double", "double[]"}; + private static final String[] CLASS_PARAMS = + new String[]{"java.lang.String", "java.lang.String[]"}; + + @Test + public void primitiveParams() { + assertNotNull(parseMethodDescriptor(TestClass.class.getName(), "primitiveParams", + PRIMITIVE_PARAMS)); + } + + @Test + public void classParams() { + assertNotNull( + parseMethodDescriptor(TestClass.class.getName(), "classParams", CLASS_PARAMS)); + } + + @Test + public void publicMethod() { + assertNotNull( + parseMethodDescriptor(TestClass.class.getName(), "publicMethod")); + } + + @Test + public void privateMethod() { + assertNotNull( + parseMethodDescriptor(TestClass.class.getName(), "privateMethod")); + } + + @Test + public void innerClass() { + assertNotNull( + parseMethodDescriptor(TestClass.class.getName() + "$InnerClass", "innerMethod", + new String[]{TestClass.class.getName() + "$InnerClass", + TestClass.class.getName() + "$InnerClass[]"})); + } + + @Test + public void interface_concreteMethod() { + assertNotNull( + parseMethodDescriptor(TestInterfaceImpl.class.getName(), "interfaceMethod")); + } + + @Test + public void interface_defaultMethod() { + assertNotNull( + parseMethodDescriptor(TestInterface.class.getName(), "defaultMethod")); + } + + @Test + public void abstractClassImpl_abstractMethod() { + assertNotNull( + parseMethodDescriptor(TestAbstractClassImpl.class.getName(), "abstractMethod")); + } + + @Test + public void abstractClass_concreteMethod() { + assertNotNull( + parseMethodDescriptor(TestAbstractClass.class.getName(), "concreteMethod")); + } + + @Test + public void notFound_illegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> parseMethodDescriptor("foo", "bar")); + assertThrows(IllegalArgumentException.class, + () -> parseMethodDescriptor(TestClass.class.getName(), "bar")); + assertThrows(IllegalArgumentException.class, + () -> parseMethodDescriptor(TestClass.class.getName(), "primitiveParams", + new String[]{"int"})); + } + + private Method parseMethodDescriptor(String fqcn, String methodName) { + return DynamicInstrumentationManagerService.parseMethodDescriptor( + getClass().getClassLoader(), + getMethodDescriptor(fqcn, methodName, new String[]{})); + } + + private Method parseMethodDescriptor(String fqcn, String methodName, String[] fqParameters) { + return DynamicInstrumentationManagerService.parseMethodDescriptor( + getClass().getClassLoader(), + getMethodDescriptor(fqcn, methodName, fqParameters)); + } + + private MethodDescriptor getMethodDescriptor(String fqcn, String methodName, + String[] fqParameters) { + MethodDescriptor methodDescriptor = new MethodDescriptor(); + methodDescriptor.fullyQualifiedClassName = fqcn; + methodDescriptor.methodName = methodName; + methodDescriptor.fullyQualifiedParameters = fqParameters; + return methodDescriptor; + } + + +} diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java new file mode 100644 index 000000000000..1be5cef28244 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static android.content.pm.Flags.FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Message; +import android.os.UserHandle; +import android.platform.test.annotations.AppModeFull; +import android.platform.test.annotations.AppModeNonSdkSandbox; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.pm.parsing.pkg.AndroidPackageInternal; +import com.android.internal.pm.pkg.component.ParsedActivity; +import com.android.server.pm.pkg.PackageStateInternal; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +@AppModeFull +@AppModeNonSdkSandbox +@RunWith(AndroidJUnit4.class) +public class BroadcastHelperTest { + private static final String TAG = "BroadcastHelperTest"; + private static final String PACKAGE_CHANGED_TEST_PACKAGE_NAME = "testpackagename"; + private static final String PACKAGE_CHANGED_TEST_MAIN_ACTIVITY = + PACKAGE_CHANGED_TEST_PACKAGE_NAME + ".MainActivity"; + private static final String PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED = + "android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Mock + ActivityManagerInternal mMockActivityManagerInternal; + @Mock + AndroidPackageInternal mMockAndroidPackageInternal; + @Mock + Computer mMockSnapshot; + @Mock + Handler mMockHandler; + @Mock + PackageManagerServiceInjector mMockPackageManagerServiceInjector; + @Mock + PackageMonitorCallbackHelper mMockPackageMonitorCallbackHelper; + @Mock + PackageStateInternal mMockPackageStateInternal; + @Mock + ParsedActivity mMockParsedActivity; + @Mock + UserManagerInternal mMockUserManagerInternal; + + private Context mContext; + private BroadcastHelper mBroadcastHelper; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + when(mMockHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer( + i -> { + ((Message) i.getArguments()[0]).getCallback().run(); + return true; + }); + when(mMockPackageManagerServiceInjector.getActivityManagerInternal()).thenReturn( + mMockActivityManagerInternal); + when(mMockPackageManagerServiceInjector.getContext()).thenReturn(mContext); + when(mMockPackageManagerServiceInjector.getHandler()).thenReturn(mMockHandler); + when(mMockPackageManagerServiceInjector.getPackageMonitorCallbackHelper()).thenReturn( + mMockPackageMonitorCallbackHelper); + when(mMockPackageManagerServiceInjector.getUserManagerInternal()).thenReturn( + mMockUserManagerInternal); + + mBroadcastHelper = new BroadcastHelper(mMockPackageManagerServiceInjector); + } + + @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES) + @Test + public void changeNonExportedComponent_sendPackageChangedBroadcastToSystem_withPermission() + throws Exception { + changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */); + + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockActivityManagerInternal).broadcastIntentWithCallback( + captor.capture(), eq(null), + eq(new String[]{PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED}), + anyInt(), eq(null), eq(null), eq(null)); + Intent intent = captor.getValue(); + assertNotNull(intent); + assertThat(intent.getPackage()).isEqualTo("android"); + } + + @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES) + @Test + public void changeNonExportedComponent_sendPackageChangedBroadcastToApplicationItself() + throws Exception { + changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */); + + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null), + eq(null), anyInt(), eq(null), eq(null), eq(null)); + Intent intent = captor.getValue(); + assertNotNull(intent); + assertThat(intent.getPackage()).isEqualTo(PACKAGE_CHANGED_TEST_PACKAGE_NAME); + } + + @Test + public void changeExportedComponent_sendPackageChangedBroadcastToAll() throws Exception { + changeComponentAndSendPackageChangedBroadcast(true /* changeExportedComponent */); + + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null), + eq(null), anyInt(), eq(null), eq(null), eq(null)); + Intent intent = captor.getValue(); + assertNotNull(intent); + assertNull(intent.getPackage()); + } + + private void changeComponentAndSendPackageChangedBroadcast(boolean changeExportedComponent) { + when(mMockSnapshot.getPackageStateInternal(eq(PACKAGE_CHANGED_TEST_PACKAGE_NAME), + anyInt())).thenReturn(mMockPackageStateInternal); + when(mMockSnapshot.isInstantAppInternal(any(), anyInt(), anyInt())).thenReturn(false); + when(mMockSnapshot.getVisibilityAllowLists(any(), any())).thenReturn(null); + when(mMockPackageStateInternal.getPkg()).thenReturn(mMockAndroidPackageInternal); + + when(mMockParsedActivity.getClassName()).thenReturn( + PACKAGE_CHANGED_TEST_MAIN_ACTIVITY); + when(mMockParsedActivity.isExported()).thenReturn(changeExportedComponent); + ArrayList<ParsedActivity> parsedActivities = new ArrayList<>(); + parsedActivities.add(mMockParsedActivity); + + when(mMockAndroidPackageInternal.getReceivers()).thenReturn(new ArrayList<>()); + when(mMockAndroidPackageInternal.getProviders()).thenReturn(new ArrayList<>()); + when(mMockAndroidPackageInternal.getServices()).thenReturn(new ArrayList<>()); + when(mMockAndroidPackageInternal.getActivities()).thenReturn(parsedActivities); + + doNothing().when(mMockPackageMonitorCallbackHelper).notifyPackageChanged(any(), + anyBoolean(), any(), anyInt(), any(), any(), any(), any(), any()); + when(mMockActivityManagerInternal.broadcastIntentWithCallback(any(), any(), any(), anyInt(), + any(), any(), any())).thenReturn(ActivityManager.BROADCAST_SUCCESS); + + ArrayList<String> componentNames = new ArrayList<>(); + componentNames.add(PACKAGE_CHANGED_TEST_MAIN_ACTIVITY); + + mBroadcastHelper.sendPackageChangedBroadcast(mMockSnapshot, + PACKAGE_CHANGED_TEST_PACKAGE_NAME, true /* dontKillApp */, componentNames, + UserHandle.USER_SYSTEM, "test" /* reason */); + } +} diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt index 09d0e4a82f7f..5a59c57ddf28 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt @@ -201,7 +201,8 @@ class PackageInstallerSessionTest { /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")), /* VerifierController */ mock(VerifierController::class.java), /* initialVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_OPEN, - /* currentVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED + /* currentVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED, + /* installDependencyHelper */ null ) } @@ -256,7 +257,8 @@ class PackageInstallerSessionTest { mTmpDir, mock(PackageSessionProvider::class.java), mock(SilentUpdatePolicy::class.java), - mock(VerifierController::class.java) + mock(VerifierController::class.java), + mock(InstallDependencyHelper::class.java) ) ret.add(session) } catch (e: Exception) { diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index be698b2673ad..6acf2421ba75 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -109,6 +109,10 @@ android_test { optimize: { enabled: false, }, + + data: [ + ":HelloWorldUsingSdk1And2", + ], } java_library { @@ -134,6 +138,7 @@ android_ravenwood_test { "androidx.annotation_annotation", "androidx.test.rules", "services.core", + "servicestests-utils-mockito-extended", ], srcs: [ "src/com/android/server/am/BroadcastRecordTest.java", diff --git a/services/tests/mockingservicestests/AndroidTest.xml b/services/tests/mockingservicestests/AndroidTest.xml index 7782d570856f..2b90119145bd 100644 --- a/services/tests/mockingservicestests/AndroidTest.xml +++ b/services/tests/mockingservicestests/AndroidTest.xml @@ -23,6 +23,12 @@ <option name="test-file-name" value="FrameworksMockingServicesTests.apk" /> </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true"/> + <option name="push-file" key="HelloWorldUsingSdk1And2.apk" + value="/data/local/tmp/tests/smockingservicestest/pm/HelloWorldUsingSdk1And2.apk"/> + </target_preparer> + <option name="test-tag" value="FrameworksMockingServicesTests" /> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java index d602660597ff..a1a8b0ec7d2f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java @@ -41,6 +41,7 @@ import android.os.TestLooperManager; import android.os.UserHandle; 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.SparseArray; @@ -97,6 +98,9 @@ public abstract class BaseBroadcastQueueTest { .spyStatic(ProcessList.class) .build(); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -112,6 +116,8 @@ public abstract class BaseBroadcastQueueTest { AlarmManagerInternal mAlarmManagerInt; @Mock ProcessList mProcessList; + @Mock + PlatformCompat mPlatformCompat; @Mock AppStartInfoTracker mAppStartInfoTracker; @@ -178,6 +184,11 @@ public abstract class BaseBroadcastQueueTest { doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any()); doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker(); + + doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt()); + doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt()); } public void tearDown() throws Exception { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 100b54897573..1481085c5f71 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -18,6 +18,7 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED; import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD; @@ -49,7 +50,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -65,7 +65,6 @@ import android.content.IIntentReceiver; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; -import android.content.pm.ResolveInfo; import android.media.AudioManager; import android.os.Bundle; import android.os.BundleMerger; @@ -73,6 +72,8 @@ import android.os.DropBoxManager; import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.util.IndentingPrintWriter; import android.util.Pair; @@ -182,10 +183,6 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { return mock(Intent.class); } - private static ResolveInfo makeMockManifestReceiver() { - return mock(ResolveInfo.class); - } - private static BroadcastFilter makeMockRegisteredReceiver() { return mock(BroadcastFilter.class); } @@ -214,7 +211,8 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, TEST_UID, false, null, null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo, Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM, - BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN); + BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN, + mPlatformCompat); } private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue, @@ -646,7 +644,8 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { @Test public void testRunnableAt_Cached_Manifest() { doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null, - List.of(makeMockManifestReceiver()), null, false), REASON_CONTAINS_MANIFEST); + List.of(makeManifestReceiver(PACKAGE_RED, CLASS_RED)), null, false), + REASON_CONTAINS_MANIFEST); } @Test @@ -679,6 +678,19 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { List.of(makeMockRegisteredReceiver()), null, false), REASON_CONTAINS_ALARM); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testRunnableAt_Cached_Prioritized_NonDeferrable_flagDisabled() { + final List receivers = List.of( + withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10), + withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10)); + final BroadcastOptions options = BroadcastOptions.makeBasic() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE); + doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, + receivers, null, false), REASON_CONTAINS_PRIORITIZED); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test public void testRunnableAt_Cached_Prioritized_NonDeferrable() { final List receivers = List.of( @@ -687,6 +699,32 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { final BroadcastOptions options = BroadcastOptions.makeBasic() .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE); doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, + receivers, null, false), REASON_CONTAINS_MANIFEST); + } + + @Test + public void testRunnableAt_Cached_Ordered_NonDeferrable() { + final List receivers = List.of( + withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10), + withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10)); + final BroadcastOptions options = BroadcastOptions.makeBasic() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE); + doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, + receivers, mock(IIntentReceiver.class), true), REASON_CONTAINS_ORDERED); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testRunnableAt_Cached_Prioritized_NonDeferrable_changeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(getUidForPackage(PACKAGE_GREEN))); + final List receivers = List.of( + withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10), + withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10)); + final BroadcastOptions options = BroadcastOptions.makeBasic() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE); + doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, receivers, null, false), REASON_CONTAINS_PRIORITIZED); } @@ -1136,6 +1174,63 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(blueQueue, List.of(screenOn)); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryGroupPolicy_prioritized_diffReceivers_flagDisabled() { + final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON); + final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF); + final BroadcastOptions screenOnOffOptions = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .setDeliveryGroupMatchingKey("screenOnOff", Intent.ACTION_SCREEN_ON); + + final Object greenReceiver = withPriority( + makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10); + final Object redReceiver = withPriority( + makeManifestReceiver(PACKAGE_RED, CLASS_RED), 5); + final Object blueReceiver = withPriority( + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false)); + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + final BroadcastProcessQueue blueQueue = mImpl.getProcessQueue(PACKAGE_BLUE, + getUidForPackage(PACKAGE_BLUE)); + verifyPendingRecords(greenQueue, List.of(screenOff)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOff)); + + assertTrue(greenQueue.isEmpty()); + assertTrue(redQueue.isEmpty()); + assertTrue(blueQueue.isEmpty()); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOff, screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOff, screenOn)); + + final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false); + screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED, + "testDeliveryGroupPolicy_prioritized_diffReceivers_flagDisabled"); + mImpl.enqueueBroadcastLocked(screenOffRecord); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOff, screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOn)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") @Test public void testDeliveryGroupPolicy_prioritized_diffReceivers() { final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON); @@ -1173,6 +1268,65 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { List.of(greenReceiver, redReceiver, blueReceiver), false)); mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOn)); + + final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false); + screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED, + "testDeliveryGroupPolicy_prioritized_diffReceivers"); + mImpl.enqueueBroadcastLocked(screenOffRecord); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOn)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryGroupPolicy_prioritized_diffReceivers_changeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(getUidForPackage(PACKAGE_GREEN))); + + final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON); + final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF); + final BroadcastOptions screenOnOffOptions = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .setDeliveryGroupMatchingKey("screenOnOff", Intent.ACTION_SCREEN_ON); + + final Object greenReceiver = withPriority( + makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10); + final Object redReceiver = withPriority( + makeManifestReceiver(PACKAGE_RED, CLASS_RED), 5); + final Object blueReceiver = withPriority( + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false)); + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + final BroadcastProcessQueue blueQueue = mImpl.getProcessQueue(PACKAGE_BLUE, + getUidForPackage(PACKAGE_BLUE)); + verifyPendingRecords(greenQueue, List.of(screenOff)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOff)); + + assertTrue(greenQueue.isEmpty()); + assertTrue(redQueue.isEmpty()); + assertTrue(blueQueue.isEmpty()); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); verifyPendingRecords(greenQueue, List.of(screenOff, screenOn)); verifyPendingRecords(redQueue, List.of(screenOff)); verifyPendingRecords(blueQueue, List.of(screenOff, screenOn)); @@ -1569,8 +1723,9 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(redQueue, List.of(userPresent, timeTick)); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test - public void testDeliveryDeferredForCached() throws Exception { + public void testDeliveryDeferredForCached_flagDisabled() throws Exception { final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); @@ -1664,8 +1819,217 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { }, false /* andRemove */); } + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryDeferredForCached_changeIdDisabled() throws Exception { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(getUidForPackage(PACKAGE_GREEN))); + + final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, + List.of(makeRegisteredReceiver(greenProcess, 0))); + + final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED); + final BroadcastOptions optionsBatteryChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged, + optionsBatteryChanged, + List.of(makeRegisteredReceiver(greenProcess, 10), + makeRegisteredReceiver(redProcess, 0)), + false /* ordered */); + + mImpl.enqueueBroadcastLocked(timeTickRecord); + mImpl.enqueueBroadcastLocked(batteryChangedRecord); + + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + true /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + // Once the broadcasts to green process are deferred, broadcasts to red process + // shouldn't be blocked anymore. + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to green process should be deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED); + final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged, + List.of(makeRegisteredReceiver(greenProcess, 0))); + mImpl.enqueueBroadcastLocked(packageChangedRecord); + + assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to the green process, including the newly enqueued one, should be + // deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + false /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + } + + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryDeferredForCached_withInfiniteDeferred_flagDisabled() throws Exception { + final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastOptions optionsTimeTick = BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, optionsTimeTick, + List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */); + + final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED); + final BroadcastOptions optionsBatteryChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged, + optionsBatteryChanged, + List.of(makeRegisteredReceiver(greenProcess, 10), + makeRegisteredReceiver(redProcess, 0)), + false /* ordered */); + + mImpl.enqueueBroadcastLocked(timeTickRecord); + mImpl.enqueueBroadcastLocked(batteryChangedRecord); + + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + true /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER, + greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + // Once the broadcasts to green process are deferred, broadcasts to red process + // shouldn't be blocked anymore. + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to green process should be deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED); + final BroadcastOptions optionsPackageChanged = + BroadcastOptions.makeWithDeferUntilActive(true); + final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged, + optionsPackageChanged, + List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */); + mImpl.enqueueBroadcastLocked(packageChangedRecord); + + assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER, + greenQueue.getRunnableAtReason()); + assertTrue(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + // All broadcasts to the green process, including the newly enqueued one, should be + // deferred. + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + + // Simulate process state change + greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */, + false /* processFreezable */); + greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply, + mImpl.mBroadcastConsumerDeferClear); + + assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason()); + assertFalse(greenQueue.shouldBeDeferred()); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason()); + assertFalse(redQueue.shouldBeDeferred()); + + greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> { + assertEquals("Unexpected state for " + r, + BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i)); + }, false /* andRemove */); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test - public void testDeliveryDeferredForCached_withInfiniteDeferred() throws Exception { + public void testDeliveryDeferredForCached_withInfiniteDeferred_changeIdDisabled() + throws Exception { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(getUidForPackage(PACKAGE_GREEN))); final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); 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 3aaf2e5c61a6..9d92d5fe4f60 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -21,6 +21,7 @@ import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER; import static android.os.UserHandle.USER_SYSTEM; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.server.am.ActivityManagerDebugConfig.LOG_WRITER_INFO; import static com.android.server.am.BroadcastProcessQueue.reasonToString; import static com.android.server.am.BroadcastRecord.deliveryStateToString; @@ -45,7 +46,6 @@ import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -77,6 +77,8 @@ import android.os.IBinder; import android.os.PowerExemptionManager; import android.os.SystemClock; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.ArrayMap; import android.util.Log; @@ -446,7 +448,8 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { callerApp.getPid(), callerApp.info.uid, false, null, null, null, null, AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo, Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId, - BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN); + BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN, + mPlatformCompat); } private void assertHealth() { @@ -1495,7 +1498,7 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { null, null, null, null, AppOpsManager.OP_NONE, BroadcastOptions.makeBasic(), List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), null, null, Activity.RESULT_OK, null, null, false, false, false, UserHandle.USER_SYSTEM, - backgroundStartPrivileges, false, null, PROCESS_STATE_UNKNOWN); + backgroundStartPrivileges, false, null, PROCESS_STATE_UNKNOWN, mPlatformCompat); enqueueBroadcast(r); waitForIdle(); @@ -1550,8 +1553,10 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { /** * Verify that when dispatching we respect tranches of priority. */ + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("DistinctVarargsChecker") @Test - public void testPriority() throws Exception { + public void testPriority_flagDisabled() throws Exception { final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); @@ -1594,6 +1599,106 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { } /** + * Verify that when dispatching we respect tranches of priority. + */ + @SuppressWarnings("DistinctVarargsChecker") + @Test + public void testOrdered_withPriorities() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW); + + // Enqueue a normal broadcast that will go to several processes, and + // then enqueue a foreground broadcast that risks reordering + final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + final IIntentReceiver orderedResultTo = mock(IIntentReceiver.class); + enqueueBroadcast(makeOrderedBroadcastRecord(timezone, callerApp, + List.of(makeRegisteredReceiver(receiverBlueApp, 10), + makeRegisteredReceiver(receiverGreenApp, 10), + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), + makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), + makeRegisteredReceiver(receiverYellowApp, -10)), + orderedResultTo, null)); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeRegisteredReceiver(receiverBlueApp)))); + waitForIdle(); + + // Ignore the final foreground broadcast + mScheduledBroadcasts.remove(makeScheduledBroadcast(receiverBlueApp, airplane)); + assertEquals(6, mScheduledBroadcasts.size()); + + // We're only concerned about enforcing ordering between tranches; + // within a tranche we're okay with reordering + assertEquals( + Set.of(makeScheduledBroadcast(receiverBlueApp, timezone), + makeScheduledBroadcast(receiverGreenApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0), + mScheduledBroadcasts.remove(0))); + assertEquals( + Set.of(makeScheduledBroadcast(receiverBlueApp, timezone), + makeScheduledBroadcast(receiverYellowApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0), + mScheduledBroadcasts.remove(0))); + assertEquals( + Set.of(makeScheduledBroadcast(receiverYellowApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0))); + } + + /** + * Verify that when dispatching we respect tranches of priority. + */ + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @SuppressWarnings("DistinctVarargsChecker") + @Test + public void testPriority_changeIdDisabled() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid)); + + // Enqueue a normal broadcast that will go to several processes, and + // then enqueue a foreground broadcast that risks reordering + final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + enqueueBroadcast(makeBroadcastRecord(timezone, callerApp, + List.of(makeRegisteredReceiver(receiverBlueApp, 10), + makeRegisteredReceiver(receiverGreenApp, 10), + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), + makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), + makeRegisteredReceiver(receiverYellowApp, -10)))); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeRegisteredReceiver(receiverBlueApp)))); + waitForIdle(); + + // Ignore the final foreground broadcast + mScheduledBroadcasts.remove(makeScheduledBroadcast(receiverBlueApp, airplane)); + assertEquals(5, mScheduledBroadcasts.size()); + + // We're only concerned about enforcing ordering between tranches; + // within a tranche we're okay with reordering + assertEquals( + Set.of(makeScheduledBroadcast(receiverBlueApp, timezone), + makeScheduledBroadcast(receiverGreenApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0), + mScheduledBroadcasts.remove(0))); + assertEquals( + Set.of(makeScheduledBroadcast(receiverBlueApp, timezone), + makeScheduledBroadcast(receiverYellowApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0), + mScheduledBroadcasts.remove(0))); + assertEquals( + Set.of(makeScheduledBroadcast(receiverYellowApp, timezone)), + Set.of(mScheduledBroadcasts.remove(0))); + } + + /** * Verify prioritized receivers work as expected with deferrable broadcast - broadcast to * app in cached state should be deferred and the rest should be delivered as per the priority * order. @@ -2305,8 +2410,35 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { .isLessThan(getReceiverScheduledTime(timeTickRecord, receiverBlue)); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testPrioritizedBroadcastDelivery_uidForeground_flagDisabled() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + + mUidObserver.onUidStateChanged(receiverGreenApp.info.uid, + ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE); + waitForIdle(); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + + final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10); + final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5); + final BroadcastRecord prioritizedRecord = makeBroadcastRecord(timeTick, callerApp, + List.of(receiverBlue, receiverGreen)); + + enqueueBroadcast(prioritizedRecord); + + waitForIdle(); + // Verify that uid foreground-ness does not impact that delivery of prioritized broadcast. + // That is, broadcast to receiverBlueApp gets scheduled before the one to receiverGreenApp. + assertThat(getReceiverScheduledTime(prioritizedRecord, receiverGreen)) + .isGreaterThan(getReceiverScheduledTime(prioritizedRecord, receiverBlue)); + } + @Test - public void testPrioritizedBroadcastDelivery_uidForeground() throws Exception { + public void testOrderedBroadcastDelivery_uidForeground() throws Exception { final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); @@ -2319,6 +2451,37 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10); final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5); + final IIntentReceiver resultTo = mock(IIntentReceiver.class); + final BroadcastRecord prioritizedRecord = makeOrderedBroadcastRecord(timeTick, callerApp, + List.of(receiverBlue, receiverGreen), resultTo, null); + + enqueueBroadcast(prioritizedRecord); + + waitForIdle(); + // Verify that uid foreground-ness does not impact that delivery of prioritized broadcast. + // That is, broadcast to receiverBlueApp gets scheduled before the one to receiverGreenApp. + assertThat(getReceiverScheduledTime(prioritizedRecord, receiverGreen)) + .isGreaterThan(getReceiverScheduledTime(prioritizedRecord, receiverBlue)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testPrioritizedBroadcastDelivery_uidForeground_changeIdDisabled() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid)); + + mUidObserver.onUidStateChanged(receiverGreenApp.info.uid, + ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE); + waitForIdle(); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + + final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10); + final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5); final BroadcastRecord prioritizedRecord = makeBroadcastRecord(timeTick, callerApp, List.of(receiverBlue, receiverGreen)); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java index 8cd0da721364..4a370a3cc431 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java @@ -18,6 +18,8 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.server.am.BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE; import static com.android.server.am.BroadcastRecord.DELIVERY_DEFERRED; import static com.android.server.am.BroadcastRecord.DELIVERY_DELIVERED; import static com.android.server.am.BroadcastRecord.DELIVERY_PENDING; @@ -33,6 +35,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import android.app.BackgroundStartPrivileges; import android.app.BroadcastOptions; @@ -46,11 +50,17 @@ import android.os.Bundle; import android.os.PersistableBundle; import android.os.Process; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.SubscriptionManager; import androidx.test.filters.SmallTest; +import com.android.server.compat.PlatformCompat; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -73,6 +83,9 @@ import java.util.function.BiFunction; public class BroadcastRecordTest { private static final String TAG = "BroadcastRecordTest"; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final int USER0 = UserHandle.USER_SYSTEM; private static final String PACKAGE1 = "pkg1"; private static final String PACKAGE2 = "pkg2"; @@ -89,10 +102,14 @@ public class BroadcastRecordTest { @Mock BroadcastQueue mQueue; @Mock ProcessRecord mProcess; + @Mock PlatformCompat mPlatformCompat; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + + doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt()); } @Test @@ -108,13 +125,13 @@ public class BroadcastRecordTest { assertArrayEquals(new int[] {-1}, calculateBlockedUntilBeyondCount(List.of( - createResolveInfo(PACKAGE1, getAppId(1), 0)), false)); + createResolveInfo(PACKAGE1, getAppId(1), 0)), false, mPlatformCompat)); assertArrayEquals(new int[] {-1}, calculateBlockedUntilBeyondCount(List.of( - createResolveInfo(PACKAGE1, getAppId(1), -10)), false)); + createResolveInfo(PACKAGE1, getAppId(1), -10)), false, mPlatformCompat)); assertArrayEquals(new int[] {-1}, calculateBlockedUntilBeyondCount(List.of( - createResolveInfo(PACKAGE1, getAppId(1), 10)), false)); + createResolveInfo(PACKAGE1, getAppId(1), 10)), false, mPlatformCompat)); } @Test @@ -128,18 +145,19 @@ public class BroadcastRecordTest { createResolveInfo(PACKAGE2, getAppId(2), 10), createResolveInfo(PACKAGE3, getAppId(3), 10)))); - assertArrayEquals(new int[] {-1,-1,-1}, + assertArrayEquals(new int[] {-1, -1, -1}, calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 0), createResolveInfo(PACKAGE2, getAppId(2), 0), - createResolveInfo(PACKAGE3, getAppId(3), 0)), false)); - assertArrayEquals(new int[] {-1,-1,-1}, + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {-1, -1, -1}, calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), createResolveInfo(PACKAGE2, getAppId(2), 10), - createResolveInfo(PACKAGE3, getAppId(3), 10)), false)); + createResolveInfo(PACKAGE3, getAppId(3), 10)), false, mPlatformCompat)); } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test public void testIsPrioritized_Yes() { assertTrue(isPrioritized(List.of( @@ -151,18 +169,203 @@ public class BroadcastRecordTest { createResolveInfo(PACKAGE2, getAppId(2), 0), createResolveInfo(PACKAGE3, getAppId(3), 0)))); - assertArrayEquals(new int[] {0,1,2}, + assertArrayEquals(new int[] {0, 1, 2}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 2, 3, 3}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities() { + assertFalse(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertFalse(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {-1, -1, -1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {-1, -1, -1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 10), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {-1, -1, -1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {-1, -1, -1, -1, -1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities_withFirstUidChangeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {0, 1, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 10), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 1, 1, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities_withLastUidChangeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3))); + + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {0, 0, 2}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 2, 3, 3}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities_withUidChangeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {0, 1, 2}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 1, 0}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 2, 2, 2}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(4), 0)), false, mPlatformCompat)); + } + + @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) + @Test + public void testIsPrioritized_withDifferentPriorities_withMultipleUidChangeIdDisabled() { + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), -10)))); + assertTrue(isPrioritized(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)))); + + assertArrayEquals(new int[] {0, 1, 2}, calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), createResolveInfo(PACKAGE2, getAppId(2), 0), - createResolveInfo(PACKAGE3, getAppId(3), -10)), false)); - assertArrayEquals(new int[] {0,0,2,3,3}, + createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 1, 1}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), + createResolveInfo(PACKAGE2, getAppId(2), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 2, 2, 2}, calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 20), createResolveInfo(PACKAGE2, getAppId(2), 20), createResolveInfo(PACKAGE3, getAppId(3), 10), createResolveInfo(PACKAGE3, getAppId(3), 0), - createResolveInfo(PACKAGE3, getAppId(3), 0)), false)); + createResolveInfo(PACKAGE3, getAppId(4), 0)), false, mPlatformCompat)); + assertArrayEquals(new int[] {0, 0, 1, 1, 3}, + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(3), 20), + createResolveInfo(PACKAGE3, getAppId(3), 10), + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(2), 0)), false, mPlatformCompat)); } @Test @@ -602,6 +805,66 @@ public class BroadcastRecordTest { assertTrue(record3.matchesDeliveryGroup(record1)); } + @Test + public void testCalculateChangeStateForReceivers() { + assertArrayEquals(new boolean[] {true, true, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + assertArrayEquals(new boolean[] {true, true, true, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + assertArrayEquals(new boolean[] {false, true, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + assertArrayEquals(new boolean[] {false, true, false, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE2, getAppId(1)), + createResolveInfo(PACKAGE3, getAppId(3))))); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + assertArrayEquals(new boolean[] {false, false, true}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + assertArrayEquals(new boolean[] {false, true, false, false, false, true}, + calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE3, getAppId(3)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE2, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + + doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( + eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3))); + assertArrayEquals(new boolean[] {false, false, false}, calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + assertArrayEquals(new boolean[] {false, false, false, false, false, false}, + calculateChangeState( + List.of(createResolveInfo(PACKAGE1, getAppId(1)), + createResolveInfo(PACKAGE3, getAppId(3)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE2, getAppId(1)), + createResolveInfo(PACKAGE2, getAppId(2)), + createResolveInfo(PACKAGE3, getAppId(3))))); + } + + private boolean[] calculateChangeState(List<Object> receivers) { + return BroadcastRecord.calculateChangeStateForReceivers(receivers, + CHANGE_LIMIT_PRIORITY_SCOPE, mPlatformCompat); + } + private static void cleanupDisabledPackageReceivers(BroadcastRecord record, String packageName, int userId) { record.cleanupDisabledPackageReceiversLocked(packageName, null /* filterByClasses */, @@ -753,16 +1016,17 @@ public class BroadcastRecordTest { BackgroundStartPrivileges.NONE, false /* timeoutExempt */, filterExtrasForReceiver, - PROCESS_STATE_UNKNOWN); + PROCESS_STATE_UNKNOWN, + mPlatformCompat); } private static int getAppId(int i) { return Process.FIRST_APPLICATION_UID + i; } - private static boolean isPrioritized(List<Object> receivers) { + private boolean isPrioritized(List<Object> receivers) { return BroadcastRecord.isPrioritized( - calculateBlockedUntilBeyondCount(receivers, false), false); + calculateBlockedUntilBeyondCount(receivers, false, mPlatformCompat), false); } private static void assertBlocked(BroadcastRecord r, boolean... blocked) { diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java new file mode 100644 index 000000000000..f6c644e3d4d4 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.pm.SharedLibraryInfo; +import android.content.pm.parsing.ApkLite; +import android.content.pm.parsing.ApkLiteParseUtils; +import android.content.pm.parsing.PackageLite; +import android.content.pm.parsing.result.ParseResult; +import android.content.pm.parsing.result.ParseTypeImpl; +import android.os.FileUtils; +import android.os.OutcomeReceiver; +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 androidx.annotation.NonNull; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Presubmit +@RunWith(JUnit4.class) +@RequiresFlagsEnabled(FLAG_SDK_DEPENDENCY_INSTALLER) +public class InstallDependencyHelperTest { + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final CheckFlagsRule checkFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + private static final String PUSH_FILE_DIR = "/data/local/tmp/tests/smockingservicestest/pm/"; + private static final String TEST_APP_USING_SDK1_AND_SDK2 = "HelloWorldUsingSdk1And2.apk"; + + @Mock private SharedLibrariesImpl mSharedLibraries; + private InstallDependencyHelper mInstallDependencyHelper; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mInstallDependencyHelper = new InstallDependencyHelper(mSharedLibraries); + } + + @Test + public void testResolveLibraryDependenciesIfNeeded_errorInSharedLibrariesImpl() + throws Exception { + doThrow(new PackageManagerException(new Exception("xyz"))) + .when(mSharedLibraries).collectMissingSharedLibraryInfos(any()); + + PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2); + CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ false); + mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback); + callback.assertFailure(); + + assertThat(callback.error).hasMessageThat().contains("xyz"); + } + + @Test + public void testResolveLibraryDependenciesIfNeeded_failsToBind() throws Exception { + // Return a non-empty list as missing dependency + PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2); + List<SharedLibraryInfo> missingDependency = Collections.singletonList( + mock(SharedLibraryInfo.class)); + when(mSharedLibraries.collectMissingSharedLibraryInfos(eq(pkg))) + .thenReturn(missingDependency); + + CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ false); + mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback); + callback.assertFailure(); + + assertThat(callback.error).hasMessageThat().contains( + "Failed to bind to Dependency Installer"); + } + + + @Test + public void testResolveLibraryDependenciesIfNeeded_allDependenciesInstalled() throws Exception { + // Return an empty list as missing dependency + PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2); + List<SharedLibraryInfo> missingDependency = Collections.emptyList(); + when(mSharedLibraries.collectMissingSharedLibraryInfos(eq(pkg))) + .thenReturn(missingDependency); + + CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ true); + mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback); + callback.assertSuccess(); + } + + private static class CallbackHelper implements OutcomeReceiver<Void, PackageManagerException> { + public PackageManagerException error; + + private final CountDownLatch mWait = new CountDownLatch(1); + private final boolean mExpectSuccess; + + CallbackHelper(boolean expectSuccess) { + mExpectSuccess = expectSuccess; + } + + @Override + public void onResult(Void result) { + if (!mExpectSuccess) { + fail("Expected to fail"); + } + mWait.countDown(); + } + + @Override + public void onError(@NonNull PackageManagerException e) { + if (mExpectSuccess) { + fail("Expected success but received: " + e); + } + error = e; + mWait.countDown(); + } + + void assertSuccess() throws Exception { + assertThat(mWait.await(1000, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(error).isNull(); + } + + void assertFailure() throws Exception { + assertThat(mWait.await(1000, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(error).isNotNull(); + } + + } + + private PackageLite getPackageLite(String apkFileName) throws Exception { + File apkFile = copyApkToTmpDir(TEST_APP_USING_SDK1_AND_SDK2); + ParseResult<ApkLite> result = ApkLiteParseUtils.parseApkLite( + ParseTypeImpl.forDefaultParsing().reset(), apkFile, 0); + assertThat(result.isError()).isFalse(); + ApkLite baseApk = result.getResult(); + + return new PackageLite(/*path=*/ null, baseApk.getPath(), baseApk, + /*splitNames=*/ null, /*isFeatureSplits=*/ null, /*usesSplitNames=*/ null, + /*configForSplit=*/ null, /*splitApkPaths=*/ null, + /*splitRevisionCodes=*/ null, baseApk.getTargetSdkVersion(), + /*requiredSplitTypes=*/ null, /*splitTypes=*/ null); + } + + private File copyApkToTmpDir(String apkFileName) throws Exception { + File outFile = temporaryFolder.newFile(apkFileName); + String apkFilePath = PUSH_FILE_DIR + apkFileName; + File apkFile = new File(apkFilePath); + assertThat(apkFile.exists()).isTrue(); + try (InputStream is = new FileInputStream(apkFile)) { + FileUtils.copyToFileOrThrow(is, outFile); + } + return outFile; + } + +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java index 591e8df1725b..71c60ad02794 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -742,10 +742,11 @@ public class StagingManagerTest { /* stagedSessionErrorMessage */ "no error", /* preVerifiedDomains */ null, /* verifierController */ null, - /* initialVerificationPolicy */ + /* initialVerificationPolicy */ PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED, /* currentVerificationPolicy */ - PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED); + PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED, + /* installDependencyHelper */ null); StagingManager.StagedSession stagedSession = spy(session.mStagedSession); doReturn(packageName).when(stagedSession).getPackageName(); diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index c645c0852f1b..9b7bbe04132c 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -114,6 +114,7 @@ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" /> + <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" /> <queries> <package android:name="com.android.servicestests.apps.suspendtestapp" /> diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 2edde9b74d0a..d5b930769e43 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -33,6 +33,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity.EXTRA_TYPE_TO_CHOOSE; @@ -80,6 +81,7 @@ import android.content.pm.ServiceInfo; import android.content.res.XmlResourceParser; import android.graphics.drawable.Icon; import android.hardware.display.DisplayManagerGlobal; +import android.hardware.input.KeyGestureEvent; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -2183,6 +2185,168 @@ public class AccessibilityManagerServiceTest { verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class); } + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_toggleMagnifier() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).containsExactly(MAGNIFICATION_CONTROLLER_NAME); + + // The magnifier will only be toggled on the second event received since the first is + // used to toggle the feature on. + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + verify(mInputFilter).notifyMagnificationShortcutTriggered(anyInt()); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_trustedService() { + setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON); + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo trustedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(trustedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + trustedService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{trustedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).containsExactly( + trustedService.getComponentName().flattenToString()); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_preinstalledService() { + setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON); + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo untrustedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(untrustedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + untrustedService.getComponentName().flattenToString()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_downloadedService() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo downloadedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ false, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(downloadedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + downloadedService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{downloadedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_defaultNotInstalled() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + final AccessibilityServiceInfo defaultService = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(installedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultSelectToSpeakService, + defaultService.getComponentName().flattenToString()); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{defaultService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) + public void handleKeyGestureEvent_activateSelectToSpeak_noDefault() { + mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); + + final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), + /* isSystemApp= */ true, /* isAlwaysOnService= */ true); + AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mInstalledServices.add(installedService); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{installedService.getComponentName().flattenToString()}); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + + mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction( + KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); + + assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE, + mA11yms.getCurrentUserIdLocked())).isEmpty(); + } private Set<String> readStringsFromSetting(String setting) { final Set<String> result = new ArraySet<>(); @@ -2298,6 +2462,10 @@ public class AccessibilityManagerServiceTest { AccessibilityManagerService service) { super(context, service); } + + @Override + void notifyMagnificationShortcutTriggered(int displayId) { + } } private static class A11yTestableContext extends TestableContext { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java index 8c35925debff..cb52eef6adfe 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java @@ -32,6 +32,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; @@ -174,6 +175,7 @@ public class AccessibilityUserStateTest { mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), SOFTWARE); mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), GESTURE); mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), QUICK_SETTINGS); + mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), KEY_GESTURE); mUserState.updateA11yTilesInQsPanelLocked( Set.of(AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME)); mUserState.setTargetAssignedToAccessibilityButton(componentNameString); @@ -201,6 +203,7 @@ public class AccessibilityUserStateTest { assertTrue(mUserState.getShortcutTargetsLocked(SOFTWARE).isEmpty()); assertTrue(mUserState.getShortcutTargetsLocked(GESTURE).isEmpty()); assertTrue(mUserState.getShortcutTargetsLocked(QUICK_SETTINGS).isEmpty()); + assertTrue(mUserState.getShortcutTargetsLocked(KEY_GESTURE).isEmpty()); assertTrue(mUserState.getA11yQsTilesInQsPanel().isEmpty()); assertNull(mUserState.getTargetAssignedToAccessibilityButton()); assertFalse(mUserState.isTouchExplorationEnabledLocked()); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java index 8164ef9314d9..f0d3456a39de 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java @@ -19,9 +19,13 @@ package com.android.server.accessibility.magnification; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; +import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MAX_VALUE; +import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MIN_VALUE; import static com.android.server.accessibility.AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID; import static com.android.server.wm.WindowManagerInternal.AccessibilityControllerInternal.UiChangesForAccessibilityCallbacks; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -660,6 +664,90 @@ public class MagnificationControllerTest { } @Test + public void scaleMagnificationByStep_fullscreenMode_stepInAndOut() throws RemoteException { + setMagnificationEnabled(MODE_FULLSCREEN); + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 1.0f, false); + reset(mScreenMagnificationController); + + // Verify the zoom scale factor increases by + // {@code MagnificationController.DefaultMagnificationScaleStepProvider + // .ZOOM_STEP_SCALE_FACTOR} and the center coordinates are + // unchanged (Float.NaN as values denotes unchanged center). + mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_IN); + verify(mScreenMagnificationController).setScaleAndCenter(eq(TEST_DISPLAY), + eq(MagnificationController + .DefaultMagnificationScaleStepProvider.ZOOM_STEP_SCALE_FACTOR), + eq(Float.NaN), eq(Float.NaN), anyBoolean(), anyInt()); + + mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_OUT); + verify(mScreenMagnificationController).setScaleAndCenter(eq(TEST_DISPLAY), + eq(SCALE_MIN_VALUE), eq(Float.NaN), eq(Float.NaN), anyBoolean(), anyInt()); + } + + @Test + public void scaleMagnificationByStep_testMaxScaling() throws RemoteException { + setMagnificationEnabled(MODE_FULLSCREEN); + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, SCALE_MIN_VALUE, false); + reset(mScreenMagnificationController); + + float currentScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + while (currentScale < SCALE_MAX_VALUE) { + assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_IN)).isTrue(); + final float nextScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + assertThat(nextScale).isGreaterThan(currentScale); + currentScale = nextScale; + } + + assertThat(currentScale).isEqualTo(SCALE_MAX_VALUE); + assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_IN)).isFalse(); + } + + @Test + public void scaleMagnificationByStep_testMinScaling() throws RemoteException { + setMagnificationEnabled(MODE_FULLSCREEN); + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, SCALE_MAX_VALUE, false); + reset(mScreenMagnificationController); + + float currentScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + while (currentScale > SCALE_MIN_VALUE) { + assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_OUT)).isTrue(); + final float nextScale = mScreenMagnificationController.getScale(TEST_DISPLAY); + assertThat(nextScale).isLessThan(currentScale); + currentScale = nextScale; + } + + assertThat(currentScale).isEqualTo(SCALE_MIN_VALUE); + assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_OUT)).isFalse(); + } + + @Test + public void scaleMagnificationByStep_windowedMode_stepInAndOut() throws RemoteException { + setMagnificationEnabled(MODE_WINDOW); + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, SCALE_MIN_VALUE, false); + reset(mMagnificationConnectionManager); + + // Verify the zoom scale factor increases by + // {@code MagnificationController.DefaultMagnificationScaleStepProvider + // .ZOOM_STEP_SCALE_FACTOR}. + mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_IN); + verify(mMagnificationConnectionManager).setScale(eq(TEST_DISPLAY), + eq(MagnificationController + .DefaultMagnificationScaleStepProvider.ZOOM_STEP_SCALE_FACTOR)); + + mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY, + MagnificationController.ZOOM_DIRECTION_OUT); + verify(mMagnificationConnectionManager).setScale(eq(TEST_DISPLAY), + eq(SCALE_MIN_VALUE)); + } + + @Test public void enableWindowMode_notifyMagnificationChanged() throws RemoteException { setMagnificationEnabled(MODE_WINDOW); diff --git a/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS b/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS deleted file mode 100644 index 0218a7835586..000000000000 --- a/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS +++ /dev/null @@ -1 +0,0 @@ -include /services/core/java/com/android/server/adaptiveauth/OWNERS
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index bc410d9168a9..88829c1a99b3 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -62,6 +62,7 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.BiometricStateListener; import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; @@ -70,8 +71,16 @@ import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceReceiver; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; +import android.hardware.biometrics.SensorProperties; import android.hardware.display.DisplayManagerGlobal; +import android.hardware.face.FaceManager; +import android.hardware.face.FaceSensorProperties; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.FingerprintSensorProperties; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.hardware.keymaster.HardwareAuthenticatorType; import android.os.Binder; import android.os.Handler; @@ -84,6 +93,7 @@ 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.security.GateKeeper; import android.security.KeyStoreAuthorization; import android.service.gatekeeper.IGateKeeperService; @@ -93,6 +103,7 @@ import android.view.Display; import android.view.DisplayInfo; import android.view.WindowManager; +import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import com.android.internal.R; @@ -111,6 +122,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -144,6 +156,27 @@ public class BiometricServiceTest { private static final int SENSOR_ID_FINGERPRINT = 0; private static final int SENSOR_ID_FACE = 1; + private final ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback.Stub> + mFingerprintAuthenticatorRegisteredCallbackCaptor = ArgumentCaptor.forClass( + IFingerprintAuthenticatorsRegisteredCallback.Stub.class); + private final ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback.Stub> + mFaceAuthenticatorRegisteredCallbackCaptor = ArgumentCaptor.forClass( + IFaceAuthenticatorsRegisteredCallback.Stub.class); + private final ArgumentCaptor<BiometricStateListener> mBiometricStateListenerArgumentCaptor = + ArgumentCaptor.forClass(BiometricStateListener.class); + private final List<FingerprintSensorPropertiesInternal> + mFingerprintSensorPropertiesInternals = List.of( + new FingerprintSensorPropertiesInternal(SENSOR_ID_FINGERPRINT, + SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, + List.of(), FingerprintSensorProperties.TYPE_UNKNOWN, + true /* resetLockoutRequiresHardwareAuthToken */)); + private final List<FaceSensorPropertiesInternal> + mFaceSensorPropertiesInternals = List.of( + new FaceSensorPropertiesInternal(SENSOR_ID_FACE, + SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, + List.of(), FaceSensorProperties.TYPE_UNKNOWN, + false /* supportsFaceDetection */, false /* supportsSelfIllumination */, + false /* resetLockoutRequiresChallenge */)); private BiometricService mBiometricService; @@ -192,6 +225,10 @@ public class BiometricServiceTest { @Mock private BiometricNotificationLogger mNotificationLogger; + @Mock + private FingerprintManager mFingerprintManager; + @Mock + private FaceManager mFaceManager; BiometricContextProvider mBiometricContextProvider; @@ -1975,6 +2012,59 @@ public class BiometricServiceTest { eq(hardwareAuthenticators)); } + @Test + public void testMandatoryBiometricsValue_whenParentProfileEnabled() throws RemoteException { + final Context context = ApplicationProvider.getApplicationContext(); + final int profileParentId = context.getContentResolver().getUserId(); + final int userId = profileParentId + 1; + final BiometricService.SettingObserver settingObserver = + new BiometricService.SettingObserver( + context, mBiometricHandlerProvider.getBiometricCallbackHandler(), + new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager); + + verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( + mFingerprintAuthenticatorRegisteredCallbackCaptor.capture()); + verify(mFaceManager).addAuthenticatorsRegisteredCallback( + mFaceAuthenticatorRegisteredCallbackCaptor.capture()); + + mFingerprintAuthenticatorRegisteredCallbackCaptor.getValue().onAllAuthenticatorsRegistered( + mFingerprintSensorPropertiesInternals); + mFaceAuthenticatorRegisteredCallbackCaptor.getValue().onAllAuthenticatorsRegistered( + mFaceSensorPropertiesInternals); + + verify(mFingerprintManager).registerBiometricStateListener( + mBiometricStateListenerArgumentCaptor.capture()); + + mBiometricStateListenerArgumentCaptor.getValue().onEnrollmentsChanged(userId, + SENSOR_ID_FINGERPRINT, true /* hasEnrollments */); + + verify(mFaceManager).registerBiometricStateListener( + mBiometricStateListenerArgumentCaptor.capture()); + + mBiometricStateListenerArgumentCaptor.getValue().onEnrollmentsChanged(userId, + SENSOR_ID_FACE, true /* hasEnrollments */); + + when(mUserManager.getProfileParent(userId)).thenReturn(new UserInfo(profileParentId, + "", 0)); + when(mUserManager.getEnabledProfileIds(profileParentId)).thenReturn(new int[]{userId}); + + //Disable Identity Check for profile user + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.MANDATORY_BIOMETRICS, 0, userId); + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, 0, + userId); + //Enable Identity Check for parent user + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.MANDATORY_BIOMETRICS, 1, profileParentId); + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, 1, + profileParentId); + + assertTrue(settingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser( + userId)); + } + // Helper methods private int invokeCanAuthenticate(BiometricService service, int authenticators) diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java index dddab657be14..5a7027edc20d 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java @@ -2158,13 +2158,11 @@ public class NetworkPolicyManagerServiceTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainEnabled() throws Exception { verify(mNetworkManager).setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true); } @Test - @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) @RequiresFlagsDisabled(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN) public void testBackgroundChainOnProcStateChangeSameDelay() throws Exception { // initialization calls setFirewallChainEnabled, so we want to reset the invocations. @@ -2194,10 +2192,7 @@ public class NetworkPolicyManagerServiceTest { } @Test - @RequiresFlagsEnabled({ - Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE, - Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN - }) + @RequiresFlagsEnabled(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN) public void testBackgroundChainOnProcStateChangeDifferentDelays() throws Exception { // The app will be blocked when there is no prior proc-state. assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); @@ -2247,7 +2242,6 @@ public class NetworkPolicyManagerServiceTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainOnAllowlistChange() throws Exception { // initialization calls setFirewallChainEnabled, so we want to reset the invocations. clearInvocations(mNetworkManager); @@ -2285,7 +2279,6 @@ public class NetworkPolicyManagerServiceTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainOnTempAllowlistChange() throws Exception { // initialization calls setFirewallChainEnabled, so we want to reset the invocations. clearInvocations(mNetworkManager); @@ -2387,7 +2380,6 @@ public class NetworkPolicyManagerServiceTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersProcStateChanges() throws Exception { int testProcStateSeq = 0; try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { @@ -2450,7 +2442,6 @@ public class NetworkPolicyManagerServiceTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersStaleChanges() throws Exception { final int testProcStateSeq = 51; try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { @@ -2470,7 +2461,6 @@ public class NetworkPolicyManagerServiceTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersCapabilityChanges() throws Exception { int testProcStateSeq = 0; try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { @@ -2559,7 +2549,6 @@ public class NetworkPolicyManagerServiceTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testObsoleteHandleUidChanged() throws Exception { callAndWaitOnUidGone(UID_A); assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); diff --git a/services/tests/servicestests/src/com/android/server/adaptiveauth/AdaptiveAuthServiceTest.java b/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationServiceTest.java index d1806881ee37..154494a13072 100644 --- a/services/tests/servicestests/src/com/android/server/adaptiveauth/AdaptiveAuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationServiceTest.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.server.adaptiveauth; +package com.android.server.security.adaptiveauthentication; import static android.adaptiveauth.Flags.FLAG_ENABLE_ADAPTIVE_AUTH; import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS; import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST; -import static com.android.server.adaptiveauth.AdaptiveAuthService.MAX_ALLOWED_FAILED_AUTH_ATTEMPTS; +import static com.android.server.security.adaptiveauthentication.AdaptiveAuthenticationService.MAX_ALLOWED_FAILED_AUTH_ATTEMPTS; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeTrue; @@ -66,12 +66,12 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** - * atest FrameworksServicesTests:AdaptiveAuthServiceTest + * atest FrameworksServicesTests:AdaptiveAuthenticationServiceTest */ @Presubmit @SmallTest @RunWith(AndroidJUnit4.class) -public class AdaptiveAuthServiceTest { +public class AdaptiveAuthenticationServiceTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -81,7 +81,7 @@ public class AdaptiveAuthServiceTest { private static final int REASON_UNKNOWN = 0; // BiometricRequestConstants.RequestReason private Context mContext; - private AdaptiveAuthService mAdaptiveAuthService; + private AdaptiveAuthenticationService mAdaptiveAuthenticationService; @Mock LockPatternUtils mLockPatternUtils; @@ -124,8 +124,9 @@ public class AdaptiveAuthServiceTest { LocalServices.removeServiceForTest(UserManagerInternal.class); LocalServices.addService(UserManagerInternal.class, mUserManager); - mAdaptiveAuthService = new AdaptiveAuthService(mContext, mLockPatternUtils); - mAdaptiveAuthService.init(); + mAdaptiveAuthenticationService = new AdaptiveAuthenticationService( + mContext, mLockPatternUtils); + mAdaptiveAuthenticationService.init(); verify(mLockSettings).registerLockSettingsStateListener( mLockSettingsStateListenerCaptor.capture()); @@ -317,13 +318,13 @@ public class AdaptiveAuthServiceTest { private void verifyNotLockDevice(int expectedCntFailedAttempts, int userId) { assertEquals(expectedCntFailedAttempts, - mAdaptiveAuthService.mFailedAttemptsForUser.get(userId)); + mAdaptiveAuthenticationService.mFailedAttemptsForUser.get(userId)); verify(mWindowManager, never()).lockNow(); } private void verifyLockDevice(int userId) { assertEquals(MAX_ALLOWED_FAILED_AUTH_ATTEMPTS, - mAdaptiveAuthService.mFailedAttemptsForUser.get(userId)); + mAdaptiveAuthenticationService.mFailedAttemptsForUser.get(userId)); verify(mLockPatternUtils).requireStrongAuth( eq(SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST), eq(userId)); // If userId is MANAGED_PROFILE_USER_ID, the StrongAuthFlag of its parent (PRIMARY_USER_ID) diff --git a/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/OWNERS b/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/OWNERS new file mode 100644 index 000000000000..bc8efa92c16f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/security/adaptiveauthentication/OWNERS
\ No newline at end of file diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java index c9d5241c57b7..b3ec2153542a 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java +++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java @@ -30,7 +30,6 @@ import android.testing.TestableContext; import androidx.test.InstrumentationRegistry; -import com.android.server.pm.UserManagerInternal; import com.android.server.uri.UriGrantsManagerInternal; import org.junit.After; @@ -42,7 +41,6 @@ import org.mockito.MockitoAnnotations; public class UiServiceTestCase { @Mock protected PackageManagerInternal mPmi; - @Mock protected UserManagerInternal mUmi; @Mock protected UriGrantsManagerInternal mUgmInternal; protected static final String PKG_N_MR1 = "com.example.n_mr1"; @@ -94,8 +92,6 @@ public class UiServiceTestCase { } }); - LocalServices.removeServiceForTest(UserManagerInternal.class); - LocalServices.addService(UserManagerInternal.class, mUmi); LocalServices.removeServiceForTest(UriGrantsManagerInternal.class); LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal); when(mUgmInternal.checkGrantUriPermission( diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index 38ff3a22022d..cc0286508cdc 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -2410,11 +2410,13 @@ public class GroupHelperTest extends UiServiceTestCase { NotificationChannel.NEWS_ID); for (NotificationRecord record: notificationList) { if (record.getChannel().getId().equals(channel1.getId()) + && record.getNotification().isGroupChild() && record.getSbn().getId() % 2 == 0) { record.updateNotificationChannel(socialChannel); mGroupHelper.onChannelUpdated(record); } if (record.getChannel().getId().equals(channel1.getId()) + && record.getNotification().isGroupChild() && record.getSbn().getId() % 2 != 0) { record.updateNotificationChannel(newsChannel); mGroupHelper.onChannelUpdated(record); @@ -2474,7 +2476,8 @@ public class GroupHelperTest extends UiServiceTestCase { NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, IMPORTANCE_DEFAULT); for (NotificationRecord record: notificationList) { - if (record.getChannel().getId().equals(channel1.getId())) { + if (record.getChannel().getId().equals(channel1.getId()) + && record.getNotification().isGroupChild()) { record.updateNotificationChannel(socialChannel); mGroupHelper.onChannelUpdated(record); } @@ -2532,7 +2535,8 @@ public class GroupHelperTest extends UiServiceTestCase { BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, NotificationChannel.SOCIAL_MEDIA_ID); for (NotificationRecord record: notificationList) { - if (record.getOriginalGroupKey().contains("testGrp")) { + if (record.getOriginalGroupKey().contains("testGrp") + && record.getNotification().isGroupChild()) { record.updateNotificationChannel(socialChannel); mGroupHelper.onChannelUpdated(record); } @@ -2631,7 +2635,8 @@ public class GroupHelperTest extends UiServiceTestCase { BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, NotificationChannel.SOCIAL_MEDIA_ID); for (NotificationRecord record: notificationList) { - if (record.getOriginalGroupKey().contains("testGrp")) { + if (record.getOriginalGroupKey().contains("testGrp") + && record.getNotification().isGroupChild()) { record.updateNotificationChannel(socialChannel); mGroupHelper.onChannelUpdated(record); } @@ -2650,6 +2655,64 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, + FLAG_NOTIFICATION_CLASSIFICATION}) + public void testValidGroupsRegrouped_notificationBundledWhileEnqueued() { + // Check that valid group notifications are regrouped if classification is done + // before onNotificationPostedWithDelay (within DELAY_FOR_ASSISTANT_TIME) + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + + final int summaryId = 0; + final int numChildren = 3; + // Post a regular/valid group: summary + notifications + NotificationRecord summary = getNotificationRecord(pkg, summaryId, + String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(summary); + summaryByGroup.put(summary.getGroupKey(), summary); + for (int i = 0; i < numChildren; i++) { + NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), + UserHandle.SYSTEM, "testGrp", false); + notificationList.add(child); + } + + // Classify/bundle child notifications. Don't call onChannelUpdated, + // adjustments applied while enqueued will use NotificationAdjustmentExtractor. + final NotificationChannel socialChannel = new NotificationChannel( + NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, + IMPORTANCE_DEFAULT); + final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); + final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( + BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + NotificationChannel.SOCIAL_MEDIA_ID); + for (NotificationRecord record: notificationList) { + if (record.getOriginalGroupKey().contains("testGrp") + && record.getNotification().isGroupChild()) { + record.updateNotificationChannel(socialChannel); + } + } + + // Check that notifications are forced grouped and app-provided summaries are canceled + for (NotificationRecord record: notificationList) { + mGroupHelper.onNotificationPostedWithDelay(record, notificationList, summaryByGroup); + } + + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social)); + verify(mCallback, times(numChildren)).addAutoGroup(anyString(), eq(expectedGroupKey_social), + eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, times(numChildren - 1)).updateAutogroupSummary(anyInt(), anyString(), + anyString(), any()); + verify(mCallback, times(numChildren)).removeAppProvidedSummaryOnClassification(anyString(), + anyString()); + } + + @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testMoveAggregateGroups_updateChannel_groupsUngrouped() { final String pkg = "package"; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index 48bc9d7c51a1..e5c42082ab97 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -1484,7 +1484,6 @@ public class ManagedServicesTest extends UiServiceTestCase { assertTrue(componentsToUnbind.get(0).contains(ComponentName.unflattenFromString("c/c"))); } - @SuppressWarnings("GuardedBy") @Test public void populateComponentsToBind() { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, @@ -1508,8 +1507,7 @@ public class ManagedServicesTest extends UiServiceTestCase { SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>(); - service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser, - /* isVisibleBackgroundUser= */ false); + service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser); assertEquals(2, componentsToBind.size()); assertEquals(1, componentsToBind.get(0).size()); @@ -1519,33 +1517,6 @@ public class ManagedServicesTest extends UiServiceTestCase { assertTrue(componentsToBind.get(10).contains(ComponentName.unflattenFromString("c/c"))); } - @SuppressWarnings("GuardedBy") - @Test - public void populateComponentsToBind_isVisibleBackgroundUser_addComponentsToBindButNotAddToEnabledComponent() { - ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, - APPROVAL_BY_COMPONENT); - - SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>(); - ArraySet<ComponentName> allowed = new ArraySet<>(); - allowed.add(ComponentName.unflattenFromString("pkg1/cmp1")); - approvedComponentsByUser.put(11, allowed); - IntArray users = new IntArray(); - users.add(11); - - SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>(); - - service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser, - /* isVisibleBackgroundUser= */ true); - - assertEquals(1, componentsToBind.size()); - assertEquals(1, componentsToBind.get(11).size()); - assertTrue(componentsToBind.get(11).contains(ComponentName.unflattenFromString( - "pkg1/cmp1"))); - assertThat(service.isComponentEnabledForCurrentProfiles( - new ComponentName("pkg1", "cmp1"))).isFalse(); - assertThat(service.isComponentEnabledForPackage("pkg1")).isFalse(); - } - @Test public void testOnNullBinding() throws Exception { Context context = mock(Context.class); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java index 411a6102f45a..361df94e8a90 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java @@ -44,7 +44,7 @@ import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; import com.android.server.UiServiceTestCase; @@ -89,7 +89,7 @@ import java.util.stream.Stream; import javax.annotation.Nullable; @RunWith(AndroidJUnit4.class) -@EnableFlags({Flags.FLAG_VISIT_PERSON_URI, Flags.FLAG_API_RICH_ONGOING}) +@EnableFlags({Flags.FLAG_API_RICH_ONGOING}) public class NotificationVisitUrisTest extends UiServiceTestCase { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java index 28ae271e20fc..cf5323e1f3a5 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java @@ -288,7 +288,6 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled */ @Test - @EnableFlags(com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE) @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER) public void testTakeScreenshot_flagEnabled() { sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0); @@ -296,17 +295,6 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { } /** - * Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled - */ - @Test - @DisableFlags({com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE, - com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER}) - public void testTakeScreenshot_flagDisabled() { - sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0); - mPhoneWindowManager.assertTakeScreenshotNotCalled(); - } - - /** * META+CTRL+BACKSPACE for taking a bugreport when the flag is enabled. */ @Test diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 6596ee935b4b..a51ce9951ab4 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -699,8 +699,8 @@ class TestPhoneWindowManager { void assertPowerWakeUp() { mTestLooper.dispatchAll(); - verify(mWindowWakeUpPolicy) - .wakeUpFromKey(anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean()); + verify(mWindowWakeUpPolicy).wakeUpFromKey( + eq(DEFAULT_DISPLAY), anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean()); } void assertNoPowerSleep() { diff --git a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java index 7322e5a3b681..3ca352cfa60d 100644 --- a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java @@ -22,6 +22,7 @@ import static android.os.PowerManager.WAKE_REASON_GESTURE; import static android.os.PowerManager.WAKE_REASON_POWER_BUTTON; import static android.os.PowerManager.WAKE_REASON_WAKE_KEY; import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.InputDevice.SOURCE_ROTARY_ENCODER; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.KeyEvent.KEYCODE_HOME; @@ -35,6 +36,7 @@ import static com.android.internal.R.bool.config_allowTheaterModeWakeFromCameraL import static com.android.internal.R.bool.config_allowTheaterModeWakeFromLidSwitch; import static com.android.internal.R.bool.config_allowTheaterModeWakeFromGesture; import static com.android.server.policy.Flags.FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE; +import static com.android.server.power.feature.flags.Flags.FLAG_PER_DISPLAY_WAKE_BY_TOUCH; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -43,6 +45,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -52,6 +55,8 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.res.Resources; import android.os.PowerManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.Display; @@ -125,6 +130,7 @@ public final class WindowWakeUpPolicyTests { } @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testMotionWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() { setTheaterModeEnabled(false); mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); @@ -136,7 +142,8 @@ public final class WindowWakeUpPolicyTests { // Verify the policy wake up call succeeds because of the call on the delegate, and not // because of a PowerManager wake up. - assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isTrue(); + assertThat(mPolicy.wakeUpFromMotion( + mDefaultDisplay.getDisplayId(), 200, SOURCE_TOUCHSCREEN, true)).isTrue(); verify(mInputWakeUpDelegate).wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true); verifyNoPowerManagerWakeUp(); @@ -144,12 +151,14 @@ public final class WindowWakeUpPolicyTests { // Verify the policy wake up call succeeds because of the PowerManager wake up, since the // delegate would not handle the wake up request. - assertThat(mPolicy.wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false)).isTrue(); + assertThat(mPolicy.wakeUpFromMotion( + mDefaultDisplay.getDisplayId(), 300, SOURCE_ROTARY_ENCODER, false)).isTrue(); verify(mInputWakeUpDelegate).wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false); verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION"); } @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testKeyWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() { setTheaterModeEnabled(false); mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); @@ -161,7 +170,7 @@ public final class WindowWakeUpPolicyTests { // Verify the policy wake up call succeeds because of the call on the delegate, and not // because of a PowerManager wake up. - assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isTrue(); + assertThat(mPolicy.wakeUpFromKey(DEFAULT_DISPLAY, 200, KEYCODE_POWER, true)).isTrue(); verify(mInputWakeUpDelegate).wakeUpFromKey(200, KEYCODE_POWER, true); verifyNoPowerManagerWakeUp(); @@ -169,7 +178,8 @@ public final class WindowWakeUpPolicyTests { // Verify the policy wake up call succeeds because of the PowerManager wake up, since the // delegate would not handle the wake up request. - assertThat(mPolicy.wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false)).isTrue(); + assertThat(mPolicy.wakeUpFromKey( + DEFAULT_DISPLAY, 300, KEYCODE_STEM_PRIMARY, false)).isTrue(); verify(mInputWakeUpDelegate).wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false); verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_KEY, "android.policy:KEY"); } @@ -186,7 +196,7 @@ public final class WindowWakeUpPolicyTests { .setInputWakeUpDelegate(mInputWakeUpDelegate); // Check that the wake up does not happen because the theater mode policy check fails. - assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isFalse(); + assertThat(mPolicy.wakeUpFromKey(DEFAULT_DISPLAY, 200, KEYCODE_POWER, true)).isFalse(); verify(mInputWakeUpDelegate, never()).wakeUpFromKey(anyLong(), anyInt(), anyBoolean()); } @@ -201,11 +211,13 @@ public final class WindowWakeUpPolicyTests { .setInputWakeUpDelegate(mInputWakeUpDelegate); // Check that the wake up does not happen because the theater mode policy check fails. - assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isFalse(); + assertThat(mPolicy.wakeUpFromMotion( + mDefaultDisplay.getDisplayId(), 200, SOURCE_TOUCHSCREEN, true)).isFalse(); verify(mInputWakeUpDelegate, never()).wakeUpFromMotion(anyLong(), anyInt(), anyBoolean()); } @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testTheaterModeChecksNotAppliedWhenScreenIsOn() { mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); setDefaultDisplayState(Display.STATE_ON); @@ -213,30 +225,69 @@ public final class WindowWakeUpPolicyTests { setBooleanRes(config_allowTheaterModeWakeFromMotion, false); mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); - mPolicy.wakeUpFromMotion(200L, SOURCE_TOUCHSCREEN, true); + mPolicy.wakeUpFromMotion(mDefaultDisplay.getDisplayId(), 200L, SOURCE_TOUCHSCREEN, true); verify(mPowerManager).wakeUp(200L, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION"); } @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testWakeUpFromMotion() { runPowerManagerUpChecks( - () -> mPolicy.wakeUpFromMotion(mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true), + () -> mPolicy.wakeUpFromMotion(mDefaultDisplay.getDisplayId(), + mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true), config_allowTheaterModeWakeFromMotion, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION"); } @Test + @EnableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) + public void testWakeUpFromMotion_perDisplayWakeByTouchEnabled() { + setTheaterModeEnabled(false); + final int displayId = 555; + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + + boolean displayWokeUp = mPolicy.wakeUpFromMotion( + displayId, mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, /* isDown= */ true); + + // Verify that display is woken up + assertThat(displayWokeUp).isTrue(); + verify(mPowerManager).wakeUp(anyLong(), eq(WAKE_REASON_WAKE_MOTION), + eq("android.policy:MOTION"), eq(displayId)); + } + + @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) + public void testWakeUpFromMotion_perDisplayWakeByTouchDisabled() { + setTheaterModeEnabled(false); + final int displayId = 555; + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + + boolean displayWokeUp = mPolicy.wakeUpFromMotion( + displayId, mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, /* isDown= */ true); + + // Verify that power is woken up and display isn't woken up individually + assertThat(displayWokeUp).isTrue(); + verify(mPowerManager).wakeUp( + anyLong(), eq(WAKE_REASON_WAKE_MOTION), eq("android.policy:MOTION")); + verify(mPowerManager, never()).wakeUp(anyLong(), eq(WAKE_REASON_WAKE_MOTION), + eq("android.policy:MOTION"), eq(displayId)); + } + + @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testWakeUpFromKey_nonPowerKey() { runPowerManagerUpChecks( - () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_HOME, true), + () -> mPolicy.wakeUpFromKey( + DEFAULT_DISPLAY, mClock.uptimeMillis(), KEYCODE_HOME, true), config_allowTheaterModeWakeFromKey, WAKE_REASON_WAKE_KEY, "android.policy:KEY"); } @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testWakeUpFromKey_powerKey() { // Disable the resource affecting all wake keys because it affects power key as well. // That way, power key wake during theater mode will solely be controlled by @@ -245,7 +296,8 @@ public final class WindowWakeUpPolicyTests { // Test with power key runPowerManagerUpChecks( - () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, true), + () -> mPolicy.wakeUpFromKey( + DEFAULT_DISPLAY, mClock.uptimeMillis(), KEYCODE_POWER, true), config_allowTheaterModeWakeFromPowerKey, WAKE_REASON_POWER_BUTTON, "android.policy:POWER"); @@ -254,13 +306,31 @@ public final class WindowWakeUpPolicyTests { // even if the power-key specific theater mode config is disabled. setBooleanRes(config_allowTheaterModeWakeFromPowerKey, false); runPowerManagerUpChecks( - () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, false), + () -> mPolicy.wakeUpFromKey( + DEFAULT_DISPLAY, mClock.uptimeMillis(), KEYCODE_POWER, false), config_allowTheaterModeWakeFromKey, WAKE_REASON_POWER_BUTTON, "android.policy:POWER"); } @Test + @EnableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) + public void testWakeUpFromKey_invalidDisplay_perDisplayWakeByTouchEnabled() { + setTheaterModeEnabled(false); + final int displayId = Display.INVALID_DISPLAY; + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + + boolean displayWokeUp = mPolicy.wakeUpFromKey( + displayId, mClock.uptimeMillis(), KEYCODE_POWER, /* isDown= */ false); + + // Verify that default display is woken up + assertThat(displayWokeUp).isTrue(); + verify(mPowerManager).wakeUp(anyLong(), eq(WAKE_REASON_POWER_BUTTON), + eq("android.policy:POWER"), eq(DEFAULT_DISPLAY)); + } + + @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testWakeUpFromLid() { runPowerManagerUpChecks( () -> mPolicy.wakeUpFromLid(), @@ -270,6 +340,7 @@ public final class WindowWakeUpPolicyTests { } @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testWakeUpFromWakeGesture() { runPowerManagerUpChecks( () -> mPolicy.wakeUpFromWakeGesture(), @@ -279,6 +350,7 @@ public final class WindowWakeUpPolicyTests { } @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testwakeUpFromCameraCover() { runPowerManagerUpChecks( () -> mPolicy.wakeUpFromCameraCover(mClock.uptimeMillis()), @@ -288,6 +360,7 @@ public final class WindowWakeUpPolicyTests { } @Test + @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH}) public void testWakeUpFromPowerKeyCameraGesture() { // Disable the resource affecting all wake keys because it affects power key as well. // That way, power key wake during theater mode will solely be controlled by diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java index 9967ccebeb1f..7dba1422d61d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java @@ -21,7 +21,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static org.junit.Assert.assertEquals; @@ -165,31 +164,6 @@ public class SurfaceAnimatorTest extends WindowTestsBase { } @Test - public void testDelayingAnimationStart() { - mAnimatable.mSurfaceAnimator.startDelayingAnimationStart(); - mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - verifyZeroInteractions(mSpec); - assertAnimating(mAnimatable); - assertTrue(mAnimatable.mSurfaceAnimator.isAnimationStartDelayed()); - mAnimatable.mSurfaceAnimator.endDelayingAnimationStart(); - verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION), any()); - } - - @Test - public void testDelayingAnimationStartAndCancelled() { - mAnimatable.mSurfaceAnimator.startDelayingAnimationStart(); - mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - mAnimatable.mSurfaceAnimator.cancelAnimation(); - verifyZeroInteractions(mSpec); - assertNotAnimating(mAnimatable); - assertTrue(mAnimatable.mFinishedCallbackCalled); - assertEquals(ANIMATION_TYPE_APP_TRANSITION, mAnimatable.mFinishedAnimationType); - verify(mTransaction).remove(eq(mAnimatable.mLeash)); - } - - @Test public void testTransferAnimation() { mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, ANIMATION_TYPE_APP_TRANSITION); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 2a06c3da0195..6490cbe3e31a 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -2183,8 +2183,8 @@ public class CarrierConfigManager { * Maximum size in bytes of the PDU to send or download when connected to a non-terrestrial * network. MmsService will return a result code of MMS_ERROR_TOO_LARGE_FOR_TRANSPORT if * the PDU exceeds this limit when connected to a non-terrestrial network. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_MMS_MAX_NTN_PAYLOAD_SIZE_BYTES_INT = "mms_max_ntn_payload_size_bytes_int"; @@ -9850,9 +9850,8 @@ public class CarrierConfigManager { * manually scanning available cellular network. * If key is {@code true}, satellite plmn should not be exposed to user and should be * automatically set, {@code false} otherwise. Default value is {@code true}. - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL = "remove_satellite_plmn_in_manual_network_scan_bool"; @@ -9877,18 +9876,18 @@ public class CarrierConfigManager { /** * Doesn't support unrestricted traffic on satellite network. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED = 0; /** * Support unrestricted but bandwidth_constrained traffic on satellite network. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_DATA_SUPPORT_BANDWIDTH_CONSTRAINED = 1; /** * Support unrestricted satellite network that serves all traffic. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_DATA_SUPPORT_ALL = 2; /** * Indicates what kind of traffic an {@link NetworkCapabilities#NET_CAPABILITY_NOT_RESTRICTED} @@ -9898,8 +9897,8 @@ public class CarrierConfigManager { * {@link ApnSetting#INFRASTRUCTURE_SATELLITE} from APN infrastructure_bitmask, and this * configuration is ignored. * By default it only supports restricted data. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_SATELLITE_DATA_SUPPORT_MODE_INT = "satellite_data_support_mode_int"; @@ -9911,8 +9910,8 @@ public class CarrierConfigManager { * {@link com.android.ims.ImsConfig.WfcModeFeatureValueConstants#WIFI_PREFERRED} * {@code false} - roaming preference can be changed by user independently and is not * overridden when device is connected to non-terrestrial network. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_OVERRIDE_WFC_ROAMING_MODE_WHILE_USING_NTN_BOOL = "override_wfc_roaming_mode_while_using_ntn_bool"; @@ -9945,8 +9944,8 @@ public class CarrierConfigManager { * Reference: GSMA TS.43-v11, 2.8.5 Fast Authentication and Token Management. * `app_name` is an optional attribute in the request and may vary depending on the carrier * requirement. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING = "satellite_entitlement_app_name_string"; @@ -9954,9 +9953,8 @@ public class CarrierConfigManager { * URL to redirect user to get more information about the carrier support for satellite. * * The default value is empty string. - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING = "satellite_information_redirect_url_string"; /** @@ -9966,9 +9964,8 @@ public class CarrierConfigManager { * This will need agreement with carriers before enabling this flag. * * The default value is false. - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL = "emergency_messaging_supported_bool"; @@ -9983,9 +9980,8 @@ public class CarrierConfigManager { * prompt user to switch to using satellite emergency messaging. * * The default value is 30 seconds. - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT = "emergency_call_to_satellite_t911_handover_timeout_millis_int"; @@ -9998,9 +9994,8 @@ public class CarrierConfigManager { * The default capabilities are * {@link NetworkRegistrationInfo#SERVICE_TYPE_SMS}, and * {@link NetworkRegistrationInfo#SERVICE_TYPE_MMS} - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_CARRIER_ROAMING_SATELLITE_DEFAULT_SERVICES_INT_ARRAY = "carrier_roaming_satellite_default_services_int_array"; @@ -10031,9 +10026,8 @@ public class CarrierConfigManager { * Defines the NIDD (Non-IP Data Delivery) APN to be used for carrier roaming to satellite * attachment. For more on NIDD, see 3GPP TS 29.542. * Note this config is the only source of truth regarding the definition of the APN. - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_SATELLITE_NIDD_APN_NAME_STRING = "satellite_nidd_apn_name_string"; @@ -10044,9 +10038,8 @@ public class CarrierConfigManager { * * If {@code false}, the emergency call is always blocked if device is in emergency satellite * mode. Note if device is NOT in emergency satellite mode, emergency call is always allowed. - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_SATELLITE_ROAMING_TURN_OFF_SESSION_FOR_EMERGENCY_CALL_BOOL = "satellite_roaming_turn_off_session_for_emergency_call_bool"; @@ -10059,14 +10052,14 @@ public class CarrierConfigManager { /** * Device can connect to carrier roaming non-terrestrial network automatically. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int CARRIER_ROAMING_NTN_CONNECT_AUTOMATIC = 0; /** * Device can connect to carrier roaming non-terrestrial network only if user manually triggers * satellite connection. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int CARRIER_ROAMING_NTN_CONNECT_MANUAL = 1; /** * Indicates carrier roaming non-terrestrial network connect type that the device can use to @@ -10074,8 +10067,8 @@ public class CarrierConfigManager { * If this key is set to CARRIER_ROAMING_NTN_CONNECT_MANUAL then connect button will be * displayed to user when the device is eligible to use carrier roaming * non-terrestrial network. - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT = "carrier_roaming_ntn_connect_type_int"; @@ -10088,7 +10081,6 @@ public class CarrierConfigManager { * will be made to T911. * * The default value is {@link SatelliteManager#EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911}. - * */ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) public static final String @@ -10105,9 +10097,8 @@ public class CarrierConfigManager { * After the timer is expired, device is marked as eligible for satellite communication. * * The default value is 180 seconds. - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_CARRIER_SUPPORTED_SATELLITE_NOTIFICATION_HYSTERESIS_SEC_INT = "carrier_supported_satellite_notification_hysteresis_sec_int"; @@ -10153,7 +10144,9 @@ public class CarrierConfigManager { "satellite_roaming_esos_inactivity_timeout_sec_int"; /** - * A string array containing the list of messaging package names that support satellite. + * A string array containing the list of messaging apps that support satellite. + * + * The default value contains only "com.google.android.apps.messaging" * * @hide */ @@ -10166,9 +10159,8 @@ public class CarrierConfigManager { * the default APN (i.e. internet) will be used for tethering. * * This config is only available when using Preset APN(not user edited) as Preferred APN. - * - * @hide */ + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final String KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL = "disable_dun_apn_while_roaming_with_preset_apn_bool"; @@ -11313,6 +11305,8 @@ public class CarrierConfigManager { NetworkRegistrationInfo.SERVICE_TYPE_SMS, NetworkRegistrationInfo.SERVICE_TYPE_MMS }); + sDefaults.putStringArray(KEY_SATELLITE_SUPPORTED_MSG_APPS_STRING_ARRAY, new String[]{ + "com.google.android.apps.messaging"}); sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false); sDefaults.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, false); sDefaults.putInt(KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT, diff --git a/telephony/java/android/telephony/satellite/EarfcnRange.aidl b/telephony/java/android/telephony/satellite/EarfcnRange.aidl new file mode 100644 index 000000000000..0b224d0b09bd --- /dev/null +++ b/telephony/java/android/telephony/satellite/EarfcnRange.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.satellite; + +parcelable EarfcnRange; diff --git a/telephony/java/android/telephony/satellite/EarfcnRange.java b/telephony/java/android/telephony/satellite/EarfcnRange.java new file mode 100644 index 000000000000..38043b570c2f --- /dev/null +++ b/telephony/java/android/telephony/satellite/EarfcnRange.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.telephony.flags.Flags; + +/** + * EARFCN (E-UTRA Absolute Radio Frequency Channel Number): A number that identifies a + * specific frequency channel in LTE/5G NR, used to define the carrier frequency. + * The range can be [0 ~ 65535] according to the 3GPP TS 36.101 + * + * In satellite communication: + * - Efficient frequency allocation across a wide coverage area. + * - Handles Doppler shift due to satellite movement. + * - Manages interference with terrestrial networks. + * + * See 3GPP TS 36.101 and 38.101-1 for details. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) +public final class EarfcnRange implements Parcelable { + + /** + * The start frequency of the earfcn range and is inclusive in the range + */ + private int mStartEarfcn; + + /** + * The end frequency of the earfcn range and is inclusive in the range. + */ + private int mEndEarfcn; + + private EarfcnRange(@NonNull Parcel in) { + readFromParcel(in); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mStartEarfcn); + dest.writeInt(mEndEarfcn); + } + + private void readFromParcel(Parcel in) { + mStartEarfcn = in.readInt(); + mEndEarfcn = in.readInt(); + } + + /** + * Constructor for the EarfcnRange class. + * The range can be [0 ~ 65535] according to the 3GPP TS 36.101 + * + * @param startEarfcn The starting earfcn value. + * @param endEarfcn The ending earfcn value. + */ + public EarfcnRange(@IntRange(from = 0, to = 65535) int endEarfcn, + @IntRange(from = 0, to = 65535) int startEarfcn) { + mEndEarfcn = endEarfcn; + mStartEarfcn = startEarfcn; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "startEarfcn: " + mStartEarfcn + ", " + "endEarfcn: " + mEndEarfcn; + } + + @NonNull + public static final Creator<EarfcnRange> CREATOR = new Creator<EarfcnRange>() { + @Override + public EarfcnRange createFromParcel(Parcel in) { + return new EarfcnRange(in); + } + + @Override + public EarfcnRange[] newArray(int size) { + return new EarfcnRange[size]; + } + }; + + /** + * Returns the starting earfcn value for this range. + * It can be [0 ~ 65535] according to the 3GPP TS 36.101 + * + * @return The starting earfcn. + */ + public @IntRange(from = 0, to = 65535) int getStartEarfcn() { + return mStartEarfcn; + } + + /** + * Returns the ending earfcn value for this range. + * It can be [0 ~ 65535] according to the 3GPP TS 36.101 + * + * @return The ending earfcn. + */ + public @IntRange(from = 0, to = 65535) int getEndEarfcn() { + return mEndEarfcn; + } +} diff --git a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl index a7eda482cb76..2730f90c4e5e 100644 --- a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl +++ b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl @@ -16,6 +16,8 @@ package android.telephony.satellite; +import android.telephony.satellite.SatelliteAccessConfiguration; + /** * Interface for satellite communication allowed state callback. * @hide @@ -29,4 +31,14 @@ oneway interface ISatelliteCommunicationAllowedStateCallback { * @param allowed whether satellite communication state or not */ void onSatelliteCommunicationAllowedStateChanged(in boolean isAllowed); + + /** + * Callback method invoked when the satellite access configuration changes + * + * @param The satellite access configuration associated with the current location. + * When satellite is not allowed at the current location, + * {@code satelliteRegionalConfiguration} will be null. + */ + void onSatelliteAccessConfigurationChanged(in SatelliteAccessConfiguration + satelliteAccessConfiguration); } diff --git a/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl new file mode 100644 index 000000000000..0214193a654f --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.telephony.satellite; + + parcelable SatelliteAccessConfiguration;
\ No newline at end of file diff --git a/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java new file mode 100644 index 000000000000..c3ae70b48854 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.telephony.flags.Flags; + +import java.util.List; + +/** + * SatelliteAccessConfiguration is used to store satellite access configuration + * that will be applied to the satellite communication at the corresponding region. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) +public final class SatelliteAccessConfiguration implements Parcelable { + /** + * The list of satellites available at the current location. + */ + @NonNull + private List<SatelliteInfo> mSatelliteInfoList; + + /** + * The list of tag IDs associated with the current location + */ + @NonNull + private int[] mTagIds; + + /** + * Constructor for {@link SatelliteAccessConfiguration}. + * + * @param satelliteInfos The list of {@link SatelliteInfo} objects representing the satellites + * accessible with this configuration. + * @param tagIds The list of tag IDs associated with this configuration. + */ + public SatelliteAccessConfiguration(@NonNull List<SatelliteInfo> satelliteInfos, + @NonNull int[] tagIds) { + mSatelliteInfoList = satelliteInfos; + mTagIds = tagIds; + } + + public SatelliteAccessConfiguration(Parcel in) { + mSatelliteInfoList = in.createTypedArrayList(SatelliteInfo.CREATOR); + mTagIds = new int[in.readInt()]; + in.readIntArray(mTagIds); + } + + public static final Creator<SatelliteAccessConfiguration> CREATOR = + new Creator<SatelliteAccessConfiguration>() { + @Override + public SatelliteAccessConfiguration createFromParcel(Parcel in) { + return new SatelliteAccessConfiguration(in); + } + + @Override + public SatelliteAccessConfiguration[] newArray(int size) { + return new SatelliteAccessConfiguration[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedList(mSatelliteInfoList); + if (mTagIds != null && mTagIds.length > 0) { + dest.writeInt(mTagIds.length); + dest.writeIntArray(mTagIds); + } else { + dest.writeInt(0); + } + } + + /** + * Returns a list of {@link SatelliteInfo} objects representing the satellites + * associated with this object. + * + * @return The list of {@link SatelliteInfo} objects. + */ + @NonNull + public List<SatelliteInfo> getSatelliteInfos() { + return mSatelliteInfoList; + } + + /** + * Returns a list of tag IDs associated with this object. + * + * @return The list of tag IDs. + */ + @NonNull + public int[] getTagIds() { + return mTagIds; + } +} diff --git a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java index 1a870202d096..bffb11f23d56 100644 --- a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java @@ -17,6 +17,7 @@ package android.telephony.satellite; import android.annotation.FlaggedApi; +import android.annotation.Nullable; import com.android.internal.telephony.flags.Flags; @@ -40,4 +41,17 @@ public interface SatelliteCommunicationAllowedStateCallback { */ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) void onSatelliteCommunicationAllowedStateChanged(boolean isAllowed); + + /** + * Callback method invoked when the satellite access configuration changes + * + * @param satelliteAccessConfiguration The satellite access configuration associated with + * the current location. When satellite is not allowed at + * the current location, + * {@code satelliteRegionalConfiguration} will be null. + * @hide + */ + @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + default void onSatelliteAccessConfigurationChanged( + @Nullable SatelliteAccessConfiguration satelliteAccessConfiguration) {}; } diff --git a/telephony/java/android/telephony/satellite/SatelliteInfo.aidl b/telephony/java/android/telephony/satellite/SatelliteInfo.aidl new file mode 100644 index 000000000000..fc2303b080a5 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.telephony.satellite; + + parcelable SatelliteInfo;
\ No newline at end of file diff --git a/telephony/java/android/telephony/satellite/SatelliteInfo.java b/telephony/java/android/telephony/satellite/SatelliteInfo.java new file mode 100644 index 000000000000..bca907e49993 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteInfo.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.telephony.flags.Flags; + +import java.util.List; +import java.util.UUID; + +/** + * SatelliteInfo stores a satellite's identification, position, and frequency information + * facilitating efficient satellite communications. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) +public class SatelliteInfo implements Parcelable { + /** + * Unique identification number for the satellite. + * This ID is used to distinguish between different satellites in the network. + */ + @NonNull + private UUID mId; + + /** + * Position information of a satellite. + * This includes the longitude and altitude of the satellite. + */ + private SatellitePosition mPosition; + + /** + * The frequency bands to scan. Bands and earfcns won't overlap. + * Bands will be filled only if the whole band is needed. + * Maximum length of the vector is 8. + */ + private int[] mBands; + + /** + * EARFCN (E-UTRA Absolute Radio Frequency Channel Number) Ranges + * The supported frequency range list. + * Maximum length of the vector is 8. + */ + private final List<EarfcnRange> mEarfcnRangeList; + + protected SatelliteInfo(Parcel in) { + ParcelUuid parcelUuid = in.readParcelable( + ParcelUuid.class.getClassLoader(), ParcelUuid.class); + if (parcelUuid != null) { + mId = parcelUuid.getUuid(); + } + mPosition = in.readParcelable(SatellitePosition.class.getClassLoader(), + SatellitePosition.class); + int numBands = in.readInt(); + mBands = new int[numBands]; + if (numBands > 0) { + for (int i = 0; i < numBands; i++) { + mBands[i] = in.readInt(); + } + } + mEarfcnRangeList = in.createTypedArrayList(EarfcnRange.CREATOR); + } + + /** + * Constructor for {@link SatelliteInfo}. + * + * @param satelliteId The ID of the satellite. + * @param satellitePosition The {@link SatellitePosition} of the satellite. + * @param bands The list of frequency bands supported by the satellite. + * @param earfcnRanges The list of {@link EarfcnRange} objects representing the EARFCN + * ranges supported by the satellite. + */ + public SatelliteInfo(@NonNull UUID satelliteId, @NonNull SatellitePosition satellitePosition, + @NonNull int[] bands, @NonNull List<EarfcnRange> earfcnRanges) { + mId = satelliteId; + mPosition = satellitePosition; + mBands = bands; + mEarfcnRangeList = earfcnRanges; + } + + public static final Creator<SatelliteInfo> CREATOR = new Creator<SatelliteInfo>() { + @Override + public SatelliteInfo createFromParcel(Parcel in) { + return new SatelliteInfo(in); + } + + @Override + public SatelliteInfo[] newArray(int size) { + return new SatelliteInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(new ParcelUuid(mId), flags); + dest.writeParcelable(mPosition, flags); + if (mBands != null && mBands.length > 0) { + dest.writeInt(mBands.length); + dest.writeIntArray(mBands); + } else { + dest.writeInt(0); + } + dest.writeTypedList(mEarfcnRangeList); + } + + /** + * Returns the ID of the satellite. + * + * @return The satellite ID. + */ + @NonNull + public UUID getSatelliteId() { + return mId; + } + + /** + * Returns the position of the satellite. + * + * @return The {@link SatellitePosition} of the satellite. + */ + public SatellitePosition getSatellitePosition() { + return mPosition; + } + + /** + * Returns the list of frequency bands supported by the satellite. + * + * @return The list of frequency bands. + */ + @NonNull + public int[] getBands() { + return mBands; + } + + /** + * Returns the list of EARFCN ranges supported by the satellite. + * + * @return The list of {@link EarfcnRange} objects. + */ + @NonNull + public List<EarfcnRange> getEarfcnRanges() { + return mEarfcnRangeList; + } +} diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 7be3f337e43a..7e3d99a5c4ac 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -36,6 +36,7 @@ import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.RemoteException; import android.os.ResultReceiver; +import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyFrameworkInitializer; @@ -272,6 +273,14 @@ public final class SatelliteManager { public static final String KEY_DEPROVISION_SATELLITE_TOKENS = "deprovision_satellite"; /** + * Bundle key to get the response from + * {@link #requestSatelliteAccessConfigurationForCurrentLocation(Executor, OutcomeReceiver)}. + * @hide + */ + public static final String KEY_SATELLITE_ACCESS_CONFIGURATION = + "satellite_access_configuration"; + + /** * The request was successfully processed. * @hide */ @@ -483,43 +492,43 @@ public final class SatelliteManager { /** * Telephony framework needs to access the current location of the device to perform the * request. However, location in the settings is disabled by users. - * * @hide */ - @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_RESULT_LOCATION_DISABLED = 25; /** * Telephony framework needs to access the current location of the device to perform the * request. However, Telephony fails to fetch the current location from location service. - * * @hide */ - @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_RESULT_LOCATION_NOT_AVAILABLE = 26; /** * Emergency call is in progress. - * * @hide */ - @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS = 27; /** * Disabling satellite is in progress. - * * @hide */ - @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_RESULT_DISABLE_IN_PROGRESS = 28; /** * Enabling satellite is in progress. - * * @hide */ - @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_RESULT_ENABLE_IN_PROGRESS = 29; /** @hide */ @@ -715,7 +724,7 @@ public final class SatelliteManager { public static final int EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911 = 2; /** - * This intent will be broadcasted if there are any change to list of subscriber informations. + * This intent will be broadcasted if there are any change to list of subscriber information. * This intent will be sent only to the app with component defined in * config_satellite_carrier_roaming_esos_provisioned_class and package defined in * config_satellite_gateway_service_package @@ -1349,12 +1358,16 @@ public final class SatelliteManager { * The satellite modem is being powered on. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8; /** * The satellite modem is being powered off. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_MODEM_STATE_DISABLING_SATELLITE = 9; /** @@ -1414,6 +1427,8 @@ public final class SatelliteManager { * there is any incoming message. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3; /** @@ -1421,6 +1436,8 @@ public final class SatelliteManager { * is the last message to emergency service provider indicating still needs help. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP = 4; /** @@ -1428,12 +1445,16 @@ public final class SatelliteManager { * is the last message to emergency service provider indicating no more help is needed. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5; /** * Datagram type indicating that the message to be sent or received is of type SMS. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int DATAGRAM_TYPE_SMS = 6; /** @@ -1441,6 +1462,8 @@ public final class SatelliteManager { * for pending incoming SMS. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS = 7; /** @hide */ @@ -1461,6 +1484,8 @@ public final class SatelliteManager { * Satellite communication restricted by user. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER = 0; /** @@ -2316,6 +2341,68 @@ public final class SatelliteManager { } /** + * Request to get satellite access configuration for the current location. + * + * @param executor The executor on which the callback will be called. + * @param callback The callback object to which the result will be delivered. + * If the request is successful, {@link OutcomeReceiver#onResult(Object)} + * will return a {@code SatelliteAccessConfiguration} with value the regional + * satellite access configuration at the current location. + * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} + * will return a {@link SatelliteException} with the {@link SatelliteResult}. + * + * @throws SecurityException if the caller doesn't have required permission. + * + * @hide + */ + @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) + @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + public void requestSatelliteAccessConfigurationForCurrentLocation( + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<SatelliteAccessConfiguration, SatelliteException> callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + try { + ITelephony telephony = getITelephony(); + if (telephony != null) { + ResultReceiver receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == SATELLITE_RESULT_SUCCESS) { + if (resultData.containsKey(KEY_SATELLITE_ACCESS_CONFIGURATION)) { + SatelliteAccessConfiguration satelliteAccessConfiguration = + resultData.getParcelable(KEY_SATELLITE_ACCESS_CONFIGURATION, + SatelliteAccessConfiguration.class); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onResult(satelliteAccessConfiguration))); + } else { + loge("KEY_SATELLITE_ACCESS_CONFIGURATION does not exist."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onError(new SatelliteException( + SATELLITE_RESULT_REQUEST_FAILED)))); + } + } else { + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onError(new SatelliteException(resultCode)))); + } + } + }; + telephony.requestSatelliteAccessConfigurationForCurrentLocation(receiver); + } else { + loge("requestSatelliteAccessConfigurationForCurrentLocation() invalid telephony"); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); + } + } catch (RemoteException ex) { + loge("requestSatelliteAccessConfigurationForCurrentLocation() RemoteException: " + + ex); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); + } + } + + /** * Request to get the duration in seconds after which the satellite will be visible. * This will be {@link Duration#ZERO} if the satellite is currently visible. * @@ -2420,7 +2507,7 @@ public final class SatelliteManager { * <li>There is no satellite communication restriction, which is added by * {@link #addAttachRestrictionForCarrier(int, int, Executor, Consumer)}</li> * <li>The carrier config {@link - * android.telephony.CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to + * CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to * {@code true}.</li> * </ul> * @@ -2743,7 +2830,7 @@ public final class SatelliteManager { * <p> * Note: This API is specifically designed for OEM enabled satellite connectivity only. * For satellite connectivity enabled using carrier roaming, please refer to - * {@link android.telephony.TelephonyCallback.SignalStrengthsListener}, and + * {@link TelephonyCallback.SignalStrengthsListener}, and * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}. * </p> * @@ -2814,7 +2901,7 @@ public final class SatelliteManager { * <p> * Note: This API is specifically designed for OEM enabled satellite connectivity only. * For satellite connectivity enabled using carrier roaming, please refer to - * {@link android.telephony.TelephonyCallback.SignalStrengthsListener}, and + * {@link TelephonyCallback.SignalStrengthsListener}, and * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}. * </p> * @@ -3131,6 +3218,15 @@ public final class SatelliteManager { () -> callback.onSatelliteCommunicationAllowedStateChanged( isAllowed))); } + + @Override + public void onSatelliteAccessConfigurationChanged( + @Nullable SatelliteAccessConfiguration + satelliteAccessConfiguration) { + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> callback.onSatelliteAccessConfigurationChanged( + satelliteAccessConfiguration))); + } }; sSatelliteCommunicationAllowedStateCallbackMap.put(callback, internalCallback); return telephony.registerForCommunicationAllowedStateChanged( diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.aidl b/telephony/java/android/telephony/satellite/SatellitePosition.aidl new file mode 100644 index 000000000000..a8028eb48ee7 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatellitePosition.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.telephony.satellite; + + parcelable SatellitePosition;
\ No newline at end of file diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.java b/telephony/java/android/telephony/satellite/SatellitePosition.java new file mode 100644 index 000000000000..1e8c0180f456 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatellitePosition.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.internal.telephony.flags.Flags; + +/** + * The position of a satellite in Earth orbit. + * + * Longitude is the angular distance, measured in degrees, east or west of the prime longitude line + * ranging from -180 to 180 degrees + * Altitude is the distance from the center of the Earth to the satellite, measured in kilometers + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) +public class SatellitePosition implements Parcelable { + + /** + * The longitude of the satellite in degrees, ranging from -180 to 180 degrees + */ + private double mLongitudeDegree; + + /** + * The distance from the center of the earth to the satellite, measured in kilometers + */ + private double mAltitudeKm; + + /** + * Constructor for {@link SatellitePosition} used to create an instance from a {@link Parcel}. + * + * @param in The {@link Parcel} to read the satellite position data from. + */ + public SatellitePosition(Parcel in) { + mLongitudeDegree = in.readDouble(); + mAltitudeKm = in.readDouble(); + } + + /** + * Constructor for {@link SatellitePosition}. + * + * @param longitudeDegree The longitude of the satellite in degrees. + * @param altitudeKm The altitude of the satellite in kilometers. + */ + public SatellitePosition(double longitudeDegree, double altitudeKm) { + mLongitudeDegree = longitudeDegree; + mAltitudeKm = altitudeKm; + } + + public static final Creator<SatellitePosition> CREATOR = new Creator<SatellitePosition>() { + @Override + public SatellitePosition createFromParcel(Parcel in) { + return new SatellitePosition(in); + } + + @Override + public SatellitePosition[] newArray(int size) { + return new SatellitePosition[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeDouble(mLongitudeDegree); + dest.writeDouble(mAltitudeKm); + } + + /** + * Returns the longitude of the satellite in degrees, ranging from -180 to 180 degrees. + * + * @return The longitude of the satellite. + */ + public double getLongitudeDegrees() { + return mLongitudeDegree; + } + + /** + * Returns the altitude of the satellite in kilometers + * + * @return The altitude of the satellite. + */ + public double getAltitudeKm() { + return mAltitudeKm; + } +} diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 62cbb02c9fc7..210200be4cf3 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -886,7 +886,7 @@ interface ITelephony { /** * @return true if the ImsService to bind to for the slot id specified was set, false otherwise. */ - boolean setBoundImsServiceOverride(int slotIndex, boolean isCarrierService, + boolean setBoundImsServiceOverride(int slotIndex, int userId, boolean isCarrierService, in int[] featureTypes, in String packageName); /** @@ -2999,6 +2999,16 @@ interface ITelephony { void requestIsCommunicationAllowedForCurrentLocation(int subId, in ResultReceiver receiver); /** + * Request to get satellite access configuration for the current location. + * + * @param receiver Result receiver to get the error code of the request + * and satellite access configuration for the current location. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.SATELLITE_COMMUNICATION)") + void requestSatelliteAccessConfigurationForCurrentLocation(in ResultReceiver receiver); + + /** * Request to get the time after which the satellite will be visible. * * @param receiver Result receiver to get the error code of the request and the requested diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 332b9b832037..9a9a331a3753 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -21,7 +21,7 @@ import android.graphics.Insets import android.graphics.Rect import android.graphics.Region import android.os.SystemClock -import android.platform.uiautomator_helpers.DeviceHelpers +import android.platform.uiautomatorhelpers.DeviceHelpers import android.tools.device.apphelpers.IStandardAppHelper import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.parsers.WindowManagerStateHelper @@ -159,7 +159,7 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ) { val caption = getCaptionForTheApp(wmHelper, device) val maximizeButton = getMaximizeButtonForTheApp(caption) - maximizeButton?.longClick() + maximizeButton.longClick() wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() val buttonResId = if (toLeft) SNAP_LEFT_BUTTON else SNAP_RIGHT_BUTTON diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 1574d1b7ce6f..09a686ca2c3f 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -757,7 +757,55 @@ class KeyGestureControllerTests { intArrayOf(KeyEvent.KEYCODE_MINUS), KeyEvent.META_ALT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) - ) + ), + TestData( + "META + ALT + '-' -> Magnifier Zoom Out", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_MINUS + ), + KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT, + intArrayOf(KeyEvent.KEYCODE_MINUS), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ALT + '=' -> Magnifier Zoom In", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_EQUALS + ), + KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN, + intArrayOf(KeyEvent.KEYCODE_EQUALS), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ALT + M -> Toggle Magnification", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_M + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + intArrayOf(KeyEvent.KEYCODE_M), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ALT + S -> Activate Select to Speak", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_S + ), + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK, + intArrayOf(KeyEvent.KEYCODE_S), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), ) } @@ -770,6 +818,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, + com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS ) @@ -787,6 +836,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, + com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS ) @@ -995,11 +1045,28 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR) ), + TestData( + "LOCK -> Lock Screen", + intArrayOf(KeyEvent.KEYCODE_LOCK), + KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, + intArrayOf(KeyEvent.KEYCODE_LOCK), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "FULLSCREEN -> Maximizes a task to fit the screen", + intArrayOf(KeyEvent.KEYCODE_FULLSCREEN), + KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_FULLSCREEN), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), ) } @Test @Parameters(method = "systemKeysTestArguments") + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) fun testSystemKeys(test: TestData) { setupKeyGestureController() testKeyGestureInternal(test) @@ -1029,6 +1096,9 @@ class KeyGestureControllerTests { KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY, KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY, KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL, + KeyEvent.KEYCODE_DO_NOT_DISTURB, + KeyEvent.KEYCODE_LOCK, + KeyEvent.KEYCODE_FULLSCREEN ) val handler = KeyGestureHandler { _, _ -> false } diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index ac96ef28f501..be5c84c0353c 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -53,7 +53,6 @@ public class TestableLooper { private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; private static final Field MESSAGE_NEXT_FIELD; private static final Field MESSAGE_WHEN_FIELD; - private static Field MESSAGE_QUEUE_USE_CONCURRENT_FIELD = null; private Looper mLooper; private MessageQueue mQueue; @@ -64,14 +63,6 @@ public class TestableLooper { static { try { - MESSAGE_QUEUE_USE_CONCURRENT_FIELD = - MessageQueue.class.getDeclaredField("mUseConcurrent"); - MESSAGE_QUEUE_USE_CONCURRENT_FIELD.setAccessible(true); - } catch (NoSuchFieldException ignored) { - // Ignore - maybe this is not CombinedMessageQueue? - } - - try { MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); @@ -155,15 +146,6 @@ public class TestableLooper { mLooper = l; mQueue = mLooper.getQueue(); mHandler = new Handler(mLooper); - - // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing. - if (MESSAGE_QUEUE_USE_CONCURRENT_FIELD != null) { - try { - MESSAGE_QUEUE_USE_CONCURRENT_FIELD.set(mQueue, false); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } } /** diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 1bcfaf60857d..56b0a25ed2dd 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -100,18 +100,6 @@ public class TestLooper { throw new RuntimeException("Reflection error constructing or accessing looper", e); } - // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing. - try { - Field messageQueueUseConcurrentField = - MessageQueue.class.getDeclaredField("mUseConcurrent"); - messageQueueUseConcurrentField.setAccessible(true); - messageQueueUseConcurrentField.set(mLooper.getQueue(), false); - } catch (NoSuchFieldException e) { - // Ignore - maybe this is not CombinedMessageQueue? - } catch (IllegalAccessException e) { - throw new RuntimeException("Reflection error constructing or accessing looper", e); - } - mClock = clock; } diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt index 1abe77fd3ceb..f260e2733843 100644 --- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -188,7 +188,7 @@ object SystemFeaturesGenerator { ?: throw IllegalArgumentException( "Invalid feature version input for $name: ${featureArgs[1]}" ) - FeatureInfo(name, featureArgs[1].toInt(), readonly = true) + FeatureInfo(name, featureVersion, readonly = true) } } } |