diff options
113 files changed, 3119 insertions, 656 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 6ecd38f054aa..3391698ee15a 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -335,6 +335,11 @@ java_aconfig_library { aconfig_declarations: "android.os.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], mode: "exported", + min_sdk_version: "30", + apex_available: [ + "//apex_available:platform", + "com.android.mediaprovider", + ], } cc_aconfig_library { @@ -716,6 +721,7 @@ aconfig_declarations { name: "android.credentials.flags-aconfig", package: "android.credentials.flags", srcs: ["core/java/android/credentials/flags.aconfig"], + exportable: true, } java_aconfig_library { @@ -724,6 +730,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "android.credentials.flags-aconfig-java-export", + aconfig_declarations: "android.credentials.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + mode: "exported", +} + // Content Protection aconfig_declarations { name: "android.view.contentprotection.flags-aconfig", diff --git a/core/api/current.txt b/core/api/current.txt index 89bde975b4d7..8a61f4a14b50 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10764,6 +10764,7 @@ package android.content { field public static final String OVERLAY_SERVICE = "overlay"; field public static final String PEOPLE_SERVICE = "people"; field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint"; + field @FlaggedApi("android.security.frp_enforcement") public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; field public static final String POWER_SERVICE = "power"; field public static final String PRINT_SERVICE = "print"; field @FlaggedApi("android.os.telemetry_apis_framework_initialization") public static final String PROFILING_SERVICE = "profiling"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8ceda62e0e02..0023e2a3d579 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3797,7 +3797,6 @@ package android.content { field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence"; field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller"; field public static final String PERMISSION_SERVICE = "permission"; - field public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness"; field public static final String ROLLBACK_SERVICE = "rollback"; field public static final String SAFETY_CENTER_SERVICE = "safety_center"; diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 1cbec3126aac..66ec865092f7 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -450,6 +450,11 @@ public final class SystemServiceRegistry { new CachedServiceFetcher<VcnManager>() { @Override public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException { + if (!ctx.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + return null; + } + IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE); IVcnManagementService service = IVcnManagementService.Stub.asInterface(b); return new VcnManager(ctx, service); @@ -1736,6 +1741,13 @@ public final class SystemServiceRegistry { return fetcher; } + private static boolean hasSystemFeatureOpportunistic(@NonNull ContextImpl ctx, + @NonNull String featureName) { + PackageManager manager = ctx.getPackageManager(); + if (manager == null) return true; + return manager.hasSystemFeature(featureName); + } + /** * Gets a system service from a given context. * @hide @@ -1758,12 +1770,18 @@ public final class SystemServiceRegistry { case Context.VIRTUALIZATION_SERVICE: case Context.VIRTUAL_DEVICE_SERVICE: return null; + case Context.VCN_MANAGEMENT_SERVICE: + if (!hasSystemFeatureOpportunistic(ctx, + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + return null; + } + break; case Context.SEARCH_SERVICE: // Wear device does not support SEARCH_SERVICE so we do not print WTF here - PackageManager manager = ctx.getPackageManager(); - if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_WATCH)) { + if (hasSystemFeatureOpportunistic(ctx, PackageManager.FEATURE_WATCH)) { return null; } + break; } Slog.wtf(TAG, "Manager wrapper not available: " + name); return null; diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl index 0dbe18156904..8bf288abb0f9 100644 --- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl +++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl @@ -53,19 +53,22 @@ void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback) = 4; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void requestFeatureDownload(in Feature feature, in ICancellationSignal signal, in IDownloadCallback callback) = 5; + void requestFeatureDownload(in Feature feature, in AndroidFuture cancellationSignalFuture, in IDownloadCallback callback) = 5; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void requestTokenInfo(in Feature feature, in Bundle requestBundle, in ICancellationSignal signal, + void requestTokenInfo(in Feature feature, in Bundle requestBundle, in AndroidFuture cancellationSignalFuture, in ITokenInfoCallback tokenInfocallback) = 6; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void processRequest(in Feature feature, in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, - in IProcessingSignal signal, in IResponseCallback responseCallback) = 7; + void processRequest(in Feature feature, in Bundle requestBundle, int requestType, + in AndroidFuture cancellationSignalFuture, + in AndroidFuture processingSignalFuture, + in IResponseCallback responseCallback) = 7; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") void processRequestStreaming(in Feature feature, - in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal signal, + in Bundle requestBundle, int requestType, in AndroidFuture cancellationSignalFuture, + in AndroidFuture processingSignalFuture, in IStreamingResponseCallback streamingCallback) = 8; String getRemoteServicePackageName() = 9; diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java index a465e3cbb6ec..bc50d2e492ae 100644 --- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java +++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java @@ -26,22 +26,23 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; -import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.IBinder; import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; import android.system.OsConstants; +import android.util.Log; import androidx.annotation.IntDef; -import com.android.internal.R; +import com.android.internal.infra.AndroidFuture; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -76,6 +77,8 @@ public final class OnDeviceIntelligenceManager { */ public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY = "AugmentRequestContentBundleKey"; + + private static final String TAG = "OnDeviceIntelligence"; private final Context mContext; private final IOnDeviceIntelligenceManager mService; @@ -121,9 +124,9 @@ public final class OnDeviceIntelligenceManager { @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public String getRemoteServicePackageName() { String result; - try{ - result = mService.getRemoteServicePackageName(); - } catch (RemoteException e){ + try { + result = mService.getRemoteServicePackageName(); + } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return result; @@ -288,18 +291,15 @@ public final class OnDeviceIntelligenceManager { } }; - ICancellationSignal transport = null; - if (cancellationSignal != null) { - transport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(transport); - } - - mService.requestFeatureDownload(feature, transport, downloadCallback); + mService.requestFeatureDownload(feature, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + downloadCallback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** * The methods computes the token related information for a given request payload using the * provided {@link Feature}. @@ -337,13 +337,9 @@ public final class OnDeviceIntelligenceManager { } }; - ICancellationSignal transport = null; - if (cancellationSignal != null) { - transport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(transport); - } - - mService.requestTokenInfo(feature, request, transport, callback); + mService.requestTokenInfo(feature, request, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -407,19 +403,9 @@ public final class OnDeviceIntelligenceManager { }; - IProcessingSignal transport = null; - if (processingSignal != null) { - transport = ProcessingSignal.createTransport(); - processingSignal.setRemote(transport); - } - - ICancellationSignal cancellationTransport = null; - if (cancellationSignal != null) { - cancellationTransport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(cancellationTransport); - } - - mService.processRequest(feature, request, requestType, cancellationTransport, transport, + mService.processRequest(feature, request, requestType, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor), callback); } catch (RemoteException e) { @@ -449,7 +435,8 @@ public final class OnDeviceIntelligenceManager { * @param callbackExecutor executor to run the callback on. */ @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) - public void processRequestStreaming(@NonNull Feature feature, @NonNull @InferenceParams Bundle request, + public void processRequestStreaming(@NonNull Feature feature, + @NonNull @InferenceParams Bundle request, @RequestType int requestType, @Nullable CancellationSignal cancellationSignal, @Nullable ProcessingSignal processingSignal, @@ -500,20 +487,11 @@ public final class OnDeviceIntelligenceManager { } }; - IProcessingSignal transport = null; - if (processingSignal != null) { - transport = ProcessingSignal.createTransport(); - processingSignal.setRemote(transport); - } - - ICancellationSignal cancellationTransport = null; - if (cancellationSignal != null) { - cancellationTransport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(cancellationTransport); - } - mService.processRequestStreaming( - feature, request, requestType, cancellationTransport, transport, callback); + feature, request, requestType, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor), + callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -574,4 +552,45 @@ public final class OnDeviceIntelligenceManager { @Target({ElementType.PARAMETER, ElementType.FIELD}) public @interface InferenceParams { } + + + @Nullable + private static AndroidFuture<IBinder> configureRemoteCancellationFuture( + @Nullable CancellationSignal cancellationSignal, + @NonNull Executor callbackExecutor) { + if (cancellationSignal == null) { + return null; + } + AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>(); + cancellationFuture.whenCompleteAsync( + (cancellationTransport, error) -> { + if (error != null || cancellationTransport == null) { + Log.e(TAG, "Unable to receive the remote cancellation signal.", error); + } else { + cancellationSignal.setRemote( + ICancellationSignal.Stub.asInterface(cancellationTransport)); + } + }, callbackExecutor); + return cancellationFuture; + } + + @Nullable + private static AndroidFuture<IBinder> configureRemoteProcessingSignalFuture( + ProcessingSignal processingSignal, Executor executor) { + if (processingSignal == null) { + return null; + } + AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>(); + processingSignalFuture.whenCompleteAsync( + (transport, error) -> { + if (error != null || transport == null) { + Log.e(TAG, "Unable to receive the remote processing signal.", error); + } else { + processingSignal.setRemote(IProcessingSignal.Stub.asInterface(transport)); + } + }, executor); + return processingSignalFuture; + } + + } diff --git a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java index c275cc786007..733f4fad96f4 100644 --- a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java +++ b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java @@ -123,10 +123,10 @@ public final class ProcessingSignal { * Sets the processing signal callback to be called when signals are received. * * This method is intended to be used by the recipient of a processing signal - * such as the remote implementation for {@link OnDeviceIntelligenceManager} to handle - * cancellation requests while performing a long-running operation. This method is not - * intended - * to be used by applications themselves. + * such as the remote implementation in + * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} to handle + * processing signals while performing a long-running operation. This method is not + * intended to be used by the caller themselves. * * If {@link ProcessingSignal#sendSignal} has already been called, then the provided callback * is invoked immediately and all previously queued actions are passed to remote signal. @@ -200,7 +200,7 @@ public final class ProcessingSignal { } /** - * Given a locally created transport, returns its associated cancellation signal. + * Given a locally created transport, returns its associated processing signal. * * @param transport The locally created transport, or null if none. * @return The associated processing signal, or null if none. diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 89300e3a15f1..284e3184d436 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4208,7 +4208,7 @@ public abstract class Context { MEDIA_COMMUNICATION_SERVICE, BATTERY_SERVICE, JOB_SCHEDULER_SERVICE, - //@hide: PERSISTENT_DATA_BLOCK_SERVICE, + PERSISTENT_DATA_BLOCK_SERVICE, //@hide: OEM_LOCK_SERVICE, MEDIA_PROJECTION_SERVICE, MIDI_SERVICE, @@ -5930,9 +5930,8 @@ public abstract class Context { * * @see #getSystemService(String) * @see android.service.persistentdata.PersistentDataBlockManager - * @hide */ - @SystemApi + @FlaggedApi(android.security.Flags.FLAG_FRP_ENFORCEMENT) public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; /** diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index 47edba6a9e56..16ca31f27028 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -47,6 +47,7 @@ flag { name: "configurable_selector_ui_enabled" description: "Enables OEM configurable Credential Selector UI" bug: "319448437" + is_exported: true } flag { diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java index b214da227a2d..689e343bcbc6 100644 --- a/core/java/android/hardware/devicestate/DeviceState.java +++ b/core/java/android/hardware/devicestate/DeviceState.java @@ -173,7 +173,7 @@ public final class DeviceState { public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17; /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN, @@ -197,7 +197,7 @@ public final class DeviceState { public @interface DeviceStateProperties {} /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN @@ -207,7 +207,7 @@ public final class DeviceState { public @interface PhysicalDeviceStateProperties {} /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS, PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP, PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL, diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index bebb912bd069..edb3a641f107 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -125,15 +125,15 @@ public final class Trace { @UnsupportedAppUsage @CriticalNative @android.ravenwood.annotation.RavenwoodReplace - private static native long nativeGetEnabledTags(); + private static native boolean nativeIsTagEnabled(long tag); @android.ravenwood.annotation.RavenwoodReplace private static native void nativeSetAppTracingAllowed(boolean allowed); @android.ravenwood.annotation.RavenwoodReplace private static native void nativeSetTracingEnabled(boolean allowed); - private static long nativeGetEnabledTags$ravenwood() { + private static boolean nativeIsTagEnabled$ravenwood(long traceTag) { // Tracing currently completely disabled under Ravenwood - return 0; + return false; } private static void nativeSetAppTracingAllowed$ravenwood(boolean allowed) { @@ -181,8 +181,7 @@ public final class Trace { @UnsupportedAppUsage @SystemApi(client = MODULE_LIBRARIES) public static boolean isTagEnabled(long traceTag) { - long tags = nativeGetEnabledTags(); - return (tags & traceTag) != 0; + return nativeIsTagEnabled(traceTag); } /** diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl index 6dbff7185f6f..908ab5f69775 100644 --- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl +++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl @@ -41,7 +41,9 @@ oneway interface IOnDeviceIntelligenceService { void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback); void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future); void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback); - void requestFeatureDownload(int callerUid, in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback); + void requestFeatureDownload(int callerUid, in Feature feature, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in IDownloadCallback downloadCallback); void registerRemoteServices(in IRemoteProcessingService remoteProcessingService); void notifyInferenceServiceConnected(); void notifyInferenceServiceDisconnected(); diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl index 799c7545968e..4213a0996e4c 100644 --- a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl +++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl @@ -24,6 +24,7 @@ import android.app.ondeviceintelligence.Feature; import android.os.ICancellationSignal; import android.os.PersistableBundle; import android.os.Bundle; +import com.android.internal.infra.AndroidFuture; import android.service.ondeviceintelligence.IRemoteStorageService; import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; @@ -34,13 +35,16 @@ import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; */ oneway interface IOnDeviceSandboxedInferenceService { void registerRemoteStorageService(in IRemoteStorageService storageService); - void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, in ICancellationSignal cancellationSignal, + void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, + in AndroidFuture<ICancellationSignal> cancellationSignal, in ITokenInfoCallback tokenInfoCallback); void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType, - in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in AndroidFuture<IProcessingSignal> processingSignal, in IResponseCallback callback); void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType, - in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in AndroidFuture<IProcessingSignal> processingSignal, in IStreamingResponseCallback callback); void updateProcessingState(in Bundle processingState, in IProcessingUpdateStatusCallback callback); diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java index 93213182d284..86320b801f6c 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java @@ -148,14 +148,18 @@ public abstract class OnDeviceIntelligenceService extends Service { @Override public void requestFeatureDownload(int callerUid, Feature feature, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, IDownloadCallback downloadCallback) { Objects.requireNonNull(feature); Objects.requireNonNull(downloadCallback); - + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } OnDeviceIntelligenceService.this.onDownloadFeature(callerUid, feature, - CancellationSignal.fromTransport(cancellationSignal), + CancellationSignal.fromTransport(transport), wrapDownloadCallback(downloadCallback)); } diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java index fc7a4c83f82c..96c45eef3731 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java @@ -122,46 +122,72 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @Override public void requestTokenInfo(int callerUid, Feature feature, Bundle request, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, ITokenInfoCallback tokenInfoCallback) { Objects.requireNonNull(feature); Objects.requireNonNull(tokenInfoCallback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } OnDeviceSandboxedInferenceService.this.onTokenInfoRequest(callerUid, feature, request, - CancellationSignal.fromTransport(cancellationSignal), + CancellationSignal.fromTransport(transport), wrapTokenInfoCallback(tokenInfoCallback)); } @Override public void processRequestStreaming(int callerUid, Feature feature, Bundle request, - int requestType, ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IStreamingResponseCallback callback) { Objects.requireNonNull(feature); Objects.requireNonNull(callback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + IProcessingSignal processingSignalTransport = null; + if (processingSignalFuture != null) { + processingSignalTransport = ProcessingSignal.createTransport(); + processingSignalFuture.complete(processingSignalTransport); + } OnDeviceSandboxedInferenceService.this.onProcessRequestStreaming(callerUid, feature, request, requestType, - CancellationSignal.fromTransport(cancellationSignal), - ProcessingSignal.fromTransport(processingSignal), + CancellationSignal.fromTransport(transport), + ProcessingSignal.fromTransport(processingSignalTransport), wrapStreamingResponseCallback(callback)); } @Override public void processRequest(int callerUid, Feature feature, Bundle request, - int requestType, ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IResponseCallback callback) { Objects.requireNonNull(feature); Objects.requireNonNull(callback); - + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + IProcessingSignal processingSignalTransport = null; + if (processingSignalFuture != null) { + processingSignalTransport = ProcessingSignal.createTransport(); + processingSignalFuture.complete(processingSignalTransport); + } OnDeviceSandboxedInferenceService.this.onProcessRequest(callerUid, feature, request, requestType, - CancellationSignal.fromTransport(cancellationSignal), - ProcessingSignal.fromTransport(processingSignal), + CancellationSignal.fromTransport(transport), + ProcessingSignal.fromTransport(processingSignalTransport), wrapResponseCallback(callback)); } @@ -206,7 +232,8 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { * Invoked when caller provides a request for a particular feature to be processed in a * streaming manner. The expectation from the implementation is that when processing the * request, - * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to continuously + * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to + * continuously * provide partial Bundle results for the caller to utilize. Optionally the implementation can * provide the complete response in the {@link StreamingProcessingCallback#onResult} upon * processing completion. diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index aff1d4a4ee12..5366a4d16754 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -126,3 +126,10 @@ flag { description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph." bug: "323376217" } + +flag { + name: "handwriting_unsupported_message" + namespace: "text" + description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it" + bug: "297962571" +} diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 29c83509dbf2..192b2ec93ce0 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -34,7 +34,9 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.Editor; import android.widget.TextView; +import android.widget.Toast; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.lang.ref.WeakReference; @@ -223,7 +225,24 @@ public class HandwritingInitiator { View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY, /* isHover */ false); if (candidateView != null && candidateView.isEnabled()) { - if (candidateView == getConnectedOrFocusedView()) { + if (shouldShowHandwritingUnavailableMessageForView(candidateView)) { + int messagesResId = (candidateView instanceof TextView tv + && tv.isAnyPasswordInputType()) + ? R.string.error_handwriting_unsupported_password + : R.string.error_handwriting_unsupported; + Toast.makeText(candidateView.getContext(), messagesResId, + Toast.LENGTH_SHORT).show(); + if (!candidateView.hasFocus()) { + requestFocusWithoutReveal(candidateView); + } + mImm.showSoftInput(candidateView, 0); + mState.mHandled = true; + mState.mShouldInitHandwriting = false; + motionEvent.setAction((motionEvent.getAction() + & MotionEvent.ACTION_POINTER_INDEX_MASK) + | MotionEvent.ACTION_CANCEL); + candidateView.getRootView().dispatchTouchEvent(motionEvent); + } else if (candidateView == getConnectedOrFocusedView()) { if (!mInitiateWithoutConnection && !candidateView.hasFocus()) { requestFocusWithoutReveal(candidateView); } @@ -484,6 +503,15 @@ public class HandwritingInitiator { return view.isStylusHandwritingAvailable(); } + private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) { + return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view); + } + + private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView( + @NonNull View view) { + return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view); + } + /** * Returns the pointer icon for the motion event, or null if it doesn't specify the icon. * This gives HandwritingInitiator a chance to show the stylus handwriting icon over a @@ -491,7 +519,7 @@ public class HandwritingInitiator { */ public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) { final View hoverView = findHoverView(event); - if (hoverView == null) { + if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) { return null; } @@ -594,7 +622,7 @@ public class HandwritingInitiator { /** * Given the location of the stylus event, return the best candidate view to initialize - * handwriting mode. + * handwriting mode or show the handwriting unavailable error message. * * @param x the x coordinates of the stylus event, in the coordinates of the window. * @param y the y coordinates of the stylus event, in the coordinates of the window. @@ -610,7 +638,8 @@ public class HandwritingInitiator { Rect handwritingArea = mTempRect; if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea) && isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover) - && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) { + && shouldTriggerHandwritingOrShowUnavailableMessageForView( + connectedOrFocusedView)) { if (!isHover && mState != null) { mState.mStylusDownWithinEditorBounds = contains(handwritingArea, x, y, 0f, 0f, 0f, 0f); @@ -628,7 +657,7 @@ public class HandwritingInitiator { final View view = viewInfo.getView(); final Rect handwritingArea = viewInfo.getHandwritingArea(); if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) - || !shouldTriggerStylusHandwritingForView(view)) { + || !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) { continue; } @@ -856,7 +885,7 @@ public class HandwritingInitiator { /** The helper method to check if the given view is still active for handwriting. */ private static boolean isViewActive(@Nullable View view) { return view != null && view.isAttachedToWindow() && view.isAggregatedVisible() - && view.shouldInitiateHandwriting(); + && view.shouldTrackHandwritingArea(); } private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) { diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS index a2f767d002f4..07d05a4ff1ea 100644 --- a/core/java/android/view/OWNERS +++ b/core/java/android/view/OWNERS @@ -75,12 +75,14 @@ per-file View.java = file:/graphics/java/android/graphics/OWNERS per-file View.java = file:/services/core/java/com/android/server/input/OWNERS per-file View.java = file:/services/core/java/com/android/server/wm/OWNERS per-file View.java = file:/core/java/android/view/inputmethod/OWNERS +per-file View.java = file:/core/java/android/text/OWNERS per-file ViewRootImpl.java = file:/services/accessibility/OWNERS per-file ViewRootImpl.java = file:/core/java/android/service/autofill/OWNERS per-file ViewRootImpl.java = file:/graphics/java/android/graphics/OWNERS per-file ViewRootImpl.java = file:/services/core/java/com/android/server/input/OWNERS per-file ViewRootImpl.java = file:/services/core/java/com/android/server/wm/OWNERS per-file ViewRootImpl.java = file:/core/java/android/view/inputmethod/OWNERS +per-file ViewRootImpl.java = file:/core/java/android/text/OWNERS per-file AccessibilityInteractionController.java = file:/services/accessibility/OWNERS per-file OnReceiveContentListener.java = file:/core/java/android/service/autofill/OWNERS per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0a75f4e6d731..41bfb24884a2 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12695,7 +12695,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (getSystemGestureExclusionRects().isEmpty() && collectPreferKeepClearRects().isEmpty() && collectUnrestrictedPreferKeepClearRects().isEmpty() - && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) { + && (info.mHandwritingArea == null || !shouldTrackHandwritingArea())) { if (info.mPositionUpdateListener != null) { mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener); info.mPositionUpdateListener = null; @@ -13062,7 +13062,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, void updateHandwritingArea() { // If autoHandwritingArea is not enabled, do nothing. - if (!shouldInitiateHandwriting()) return; + if (!shouldTrackHandwritingArea()) return; final AttachInfo ai = mAttachInfo; if (ai != null) { ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this); @@ -13080,6 +13080,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Returns whether the handwriting initiator should track the handwriting area for this view, + * either to initiate handwriting mode, or to prepare handwriting delegation, or to show the + * handwriting unsupported message. + * @hide + */ + public boolean shouldTrackHandwritingArea() { + return shouldInitiateHandwriting(); + } + + /** * Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this * view's bounds. The callback will be called from the UI thread. * diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 0373539c44ea..52604702f2c1 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -13565,6 +13565,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** @hide */ @Override + public boolean shouldTrackHandwritingArea() { + // The handwriting initiator tracks all editable TextViews regardless of whether handwriting + // is supported, so that it can show an error message for unsupported editable TextViews. + return super.shouldTrackHandwritingArea() + || (Flags.handwritingUnsupportedMessage() && onCheckIsTextEditor()); + } + + /** @hide */ + @Override public boolean isStylusHandwritingAvailable() { if (mTextOperationUser == null) { return super.isStylusHandwritingAvailable(); diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 52237989f059..d48cdc4645c6 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -161,6 +161,7 @@ static struct { jfieldID mMixType; jfieldID mCallbackFlags; jfieldID mToken; + jfieldID mVirtualDeviceId; } gAudioMixFields; static jclass gAudioFormatClass; @@ -2312,7 +2313,7 @@ static jint convertAudioMixFromNative(JNIEnv *env, jobject *jAudioMix, const Aud jstring deviceAddress = env->NewStringUTF(nAudioMix.mDeviceAddress.c_str()); *jAudioMix = env->NewObject(gAudioMixClass, gAudioMixCstor, jAudioMixingRule, jAudioFormat, nAudioMix.mRouteFlags, nAudioMix.mCbFlags, nAudioMix.mDeviceType, - deviceAddress, jBinderToken); + deviceAddress, jBinderToken, nAudioMix.mVirtualDeviceId); return AUDIO_JAVA_SUCCESS; } @@ -2347,6 +2348,7 @@ static jint convertAudioMixToNative(JNIEnv *env, AudioMix *nAudioMix, const jobj aiBinder(AIBinder_fromJavaBinder(env, jToken), &AIBinder_decStrong); nAudioMix->mToken = AIBinder_toPlatformBinder(aiBinder.get()); + nAudioMix->mVirtualDeviceId = env->GetIntField(jAudioMix, gAudioMixFields.mVirtualDeviceId); jint status = convertAudioMixingRuleToNative(env, jRule, &(nAudioMix->mCriteria)); env->DeleteLocalRef(jRule); @@ -3676,7 +3678,7 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixCstor = GetMethodIDOrDie(env, audioMixClass, "<init>", "(Landroid/media/audiopolicy/AudioMixingRule;Landroid/" - "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;)V"); + "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;I)V"); } gAudioMixFields.mRule = GetFieldIDOrDie(env, audioMixClass, "mRule", "Landroid/media/audiopolicy/AudioMixingRule;"); @@ -3689,6 +3691,7 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixFields.mMixType = GetFieldIDOrDie(env, audioMixClass, "mMixType", "I"); gAudioMixFields.mCallbackFlags = GetFieldIDOrDie(env, audioMixClass, "mCallbackFlags", "I"); gAudioMixFields.mToken = GetFieldIDOrDie(env, audioMixClass, "mToken", "Landroid/os/IBinder;"); + gAudioMixFields.mVirtualDeviceId = GetFieldIDOrDie(env, audioMixClass, "mVirtualDeviceId", "I"); jclass audioFormatClass = FindClassOrDie(env, "android/media/AudioFormat"); gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass); diff --git a/core/jni/android_os_Trace.cpp b/core/jni/android_os_Trace.cpp index b579daf505e7..4387a4c63673 100644 --- a/core/jni/android_os_Trace.cpp +++ b/core/jni/android_os_Trace.cpp @@ -124,8 +124,8 @@ static void android_os_Trace_nativeInstantForTrack(JNIEnv* env, jclass, }); } -static jlong android_os_Trace_nativeGetEnabledTags(JNIEnv* env) { - return tracing_perfetto::getEnabledCategories(); +static jboolean android_os_Trace_nativeIsTagEnabled(jlong tag) { + return tracing_perfetto::isTagEnabled(tag); } static void android_os_Trace_nativeRegisterWithPerfetto(JNIEnv* env) { @@ -157,7 +157,7 @@ static const JNINativeMethod gTraceMethods[] = { {"nativeRegisterWithPerfetto", "()V", (void*)android_os_Trace_nativeRegisterWithPerfetto}, // ----------- @CriticalNative ---------------- - {"nativeGetEnabledTags", "()J", (void*)android_os_Trace_nativeGetEnabledTags}, + {"nativeIsTagEnabled", "(J)Z", (void*)android_os_Trace_nativeIsTagEnabled}, }; int register_android_os_Trace(JNIEnv* env) { diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 5639a583f296..a3dba48bbb7d 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3249,6 +3249,12 @@ <!-- Title for EditText context menu [CHAR LIMIT=20] --> <string name="editTextMenuTitle">Text actions</string> + <!-- Error shown when a user uses a stylus to try handwriting on a text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] --> + <string name="error_handwriting_unsupported">Handwriting is not supported in this field</string> + + <!-- Error shown when a user uses a stylus to try handwriting on a password text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] --> + <string name="error_handwriting_unsupported_password">Handwriting is not supported in password fields</string> + <!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="input_method_nav_back_button_desc">Back</string> <!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 668a88c4370a..c2fa297ea984 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3122,6 +3122,8 @@ <!-- TextView --> <java-symbol type="bool" name="config_textShareSupported" /> <java-symbol type="string" name="failed_to_copy_to_clipboard" /> + <java-symbol type="string" name="error_handwriting_unsupported" /> + <java-symbol type="string" name="error_handwriting_unsupported_password" /> <java-symbol type="id" name="notification_material_reply_container" /> <java-symbol type="id" name="notification_material_reply_text_1" /> diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index a5c962412024..6c00fd80c5e1 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -55,6 +55,7 @@ import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -72,6 +73,7 @@ import org.mockito.ArgumentCaptor; */ @Presubmit @SmallTest +@UiThreadTest @RunWith(AndroidJUnit4.class) public class HandwritingInitiatorTest { private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout(); diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 9c1c700641f1..ea3235bfff6c 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -588,6 +588,8 @@ applications that come with the platform <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" /> <!-- Permission required for CTS test - PackageManagerShellCommandInstallTest --> <permission name="android.permission.EMERGENCY_INSTALL_PACKAGES" /> + <!-- Permission required for Cts test - CtsSettingsTestCases --> + <permission name="android.permission.PREPARE_FACTORY_RESET" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/keyboards/Vendor_054c_Product_05c4.idc b/data/keyboards/Vendor_054c_Product_05c4.idc index 9576e8d042ba..2da622745baf 100644 --- a/data/keyboards/Vendor_054c_Product_05c4.idc +++ b/data/keyboards/Vendor_054c_Product_05c4.idc @@ -45,14 +45,15 @@ sensor.gyroscope.power = 0.8 # This uneven timing causes the apparent speed of a finger (calculated using # time deltas between received reports) to vary dramatically even if it's # actually moving smoothly across the touchpad, triggering the touchpad stack's -# drumroll detection logic, which causes the finger's single smooth movement to -# be treated as many small movements of consecutive touches, which are then -# inhibited by the click wiggle filter. +# drumroll detection logic. For moving fingers, the drumroll detection logic +# splits the finger's single movement into many small movements of consecutive +# touches, which are then inhibited by the click wiggle filter. For tapping +# fingers, it prevents tapping to click because it thinks the finger's moving +# too fast. # -# Since this touchpad does not seem vulnerable to click wiggle, we can safely -# disable drumroll detection due to speed changes (by setting the speed change -# threshold very high, since there's no boolean control property). -gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000 +# Since this touchpad doesn't seem to have to drumroll issues, we can safely +# disable drumroll detection. +gestureProp.Drumroll_Suppression_Enable = 0 # Because of the way this touchpad is positioned, touches around the edges are # no more likely to be palms than ones in the middle, so remove the edge zones diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc index 9576e8d042ba..2a1a4fc62b24 100644 --- a/data/keyboards/Vendor_054c_Product_09cc.idc +++ b/data/keyboards/Vendor_054c_Product_09cc.idc @@ -45,14 +45,15 @@ sensor.gyroscope.power = 0.8 # This uneven timing causes the apparent speed of a finger (calculated using # time deltas between received reports) to vary dramatically even if it's # actually moving smoothly across the touchpad, triggering the touchpad stack's -# drumroll detection logic, which causes the finger's single smooth movement to -# be treated as many small movements of consecutive touches, which are then -# inhibited by the click wiggle filter. +# drumroll detection logic. For moving fingers, the drumroll detection logic +# splits the finger's single movement into many small movements of consecutive +# touches, which are then inhibited by the click wiggle filter. For tapping +# fingers, it prevents tapping to click because it thinks the finger's moving +# too fast. # -# Since this touchpad does not seem vulnerable to click wiggle, we can safely -# disable drumroll detection due to speed changes (by setting the speed change -# threshold very high, since there's no boolean control property). -gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000 +# Since this touchpad doesn't seem to have drumroll issues, we can safely +# disable drumroll detection. +gestureProp.Drumroll_Suppression_Enable = 0 # Because of the way this touchpad is positioned, touches around the edges are # no more likely to be palms than ones in the middle, so remove the edge zones diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt index 1ccc7d8084a6..5f25d70acf7c 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt @@ -24,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.WindowUtils import android.tools.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -181,6 +182,12 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : } } + /** {@inheritDoc} */ + @FlakyTest(bugId = 312446524) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 6f7024ae76b4..1fe3c2ecec29 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -5453,7 +5453,8 @@ public class AudioManager { String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(), policy.hasFocusListener(), policy.isFocusPolicy(), policy.isTestFocusPolicy(), policy.isVolumeController(), - projection == null ? null : projection.getProjection()); + projection == null ? null : projection.getProjection(), + policy.getAttributionSource()); if (regId == null) { return ERROR; } else { diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index 447d3bbddceb..80e57193d0dc 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -789,7 +789,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, private @NonNull AudioRecord buildAudioPlaybackCaptureRecord() { AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat); MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection(); - AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null) + AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ mContext) .setMediaProjection(projection) .addMix(audioMix).build(); @@ -853,7 +853,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, .setFormat(mFormat) .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK) .build(); - AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build(); + AudioPolicy audioPolicy = new AudioPolicy.Builder(mContext).addMix(audioMix).build(); if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) { throw new UnsupportedOperationException("Error: could not register audio policy"); } diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 194da217a121..73deb17d0055 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -1353,7 +1353,8 @@ public class AudioTrack extends PlayerBase .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK) .build(); AudioPolicy audioPolicy = - new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build(); + new AudioPolicy.Builder(/*context=*/ mContext).addMix(audioMix).build(); + if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) { throw new UnsupportedOperationException("Error: could not register audio policy"); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 98bd3caf3f7d..e612645fb4d7 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -18,6 +18,7 @@ package android.media; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; +import android.content.AttributionSource; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioFormat; @@ -361,7 +362,8 @@ interface IAudioService { String registerAudioPolicy(in AudioPolicyConfig policyConfig, in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, in IMediaProjection projection); + boolean isVolumeController, in IMediaProjection projection, + in AttributionSource attributionSource); oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb); diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index ab7c27f70e05..2d7db5e6ed94 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -35,6 +35,7 @@ import android.media.tv.tunerresourcemanager.TunerResourceManager; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.os.IBinder; import android.os.IHwBinder; import android.os.Looper; import android.os.Message; @@ -43,7 +44,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.util.Log; -import android.util.Singleton; import com.android.internal.util.FrameworkStatsLog; @@ -264,71 +264,107 @@ public final class MediaCas implements AutoCloseable { public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; - private static final Singleton<IMediaCasService> sService = - new Singleton<IMediaCasService>() { + private static IMediaCasService sService = null; + private static Object sAidlLock = new Object(); + + /** DeathListener for AIDL service */ + private static IBinder.DeathRecipient sDeathListener = + new IBinder.DeathRecipient() { @Override - protected IMediaCasService create() { - try { - Log.d(TAG, "Trying to get AIDL service"); - IMediaCasService serviceAidl = - IMediaCasService.Stub.asInterface( - ServiceManager.waitForDeclaredService( - IMediaCasService.DESCRIPTOR + "/default")); - if (serviceAidl != null) { - return serviceAidl; - } - } catch (Exception eAidl) { - Log.d(TAG, "Failed to get cas AIDL service"); + public void binderDied() { + synchronized (sAidlLock) { + Log.d(TAG, "The service is dead"); + sService.asBinder().unlinkToDeath(sDeathListener, 0); + sService = null; } - return null; } }; - private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl = - new Singleton<android.hardware.cas.V1_0.IMediaCasService>() { - @Override - protected android.hardware.cas.V1_0.IMediaCasService create() { - try { - Log.d(TAG, "Trying to get cas@1.2 service"); - android.hardware.cas.V1_2.IMediaCasService serviceV12 = - android.hardware.cas.V1_2.IMediaCasService.getService( - true /*wait*/); - if (serviceV12 != null) { - return serviceV12; - } - } catch (Exception eV1_2) { - Log.d(TAG, "Failed to get cas@1.2 service"); + static IMediaCasService getService() { + synchronized (sAidlLock) { + if (sService == null || !sService.asBinder().isBinderAlive()) { + try { + Log.d(TAG, "Trying to get AIDL service"); + sService = + IMediaCasService.Stub.asInterface( + ServiceManager.waitForDeclaredService( + IMediaCasService.DESCRIPTOR + "/default")); + if (sService != null) { + sService.asBinder().linkToDeath(sDeathListener, 0); } + } catch (Exception eAidl) { + Log.d(TAG, "Failed to get cas AIDL service"); + } + } + return sService; + } + } - try { - Log.d(TAG, "Trying to get cas@1.1 service"); - android.hardware.cas.V1_1.IMediaCasService serviceV11 = - android.hardware.cas.V1_1.IMediaCasService.getService( - true /*wait*/); - if (serviceV11 != null) { - return serviceV11; + private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null; + private static Object sHidlLock = new Object(); + + /** Used to indicate the right end-point to handle the serviceDied method */ + private static final long MEDIA_CAS_HIDL_COOKIE = 394; + + /** DeathListener for HIDL service */ + private static IHwBinder.DeathRecipient sDeathListenerHidl = + new IHwBinder.DeathRecipient() { + @Override + public void serviceDied(long cookie) { + if (cookie == MEDIA_CAS_HIDL_COOKIE) { + synchronized (sHidlLock) { + sServiceHidl = null; } - } catch (Exception eV1_1) { - Log.d(TAG, "Failed to get cas@1.1 service"); } + } + }; - try { - Log.d(TAG, "Trying to get cas@1.0 service"); - return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); - } catch (Exception eV1_0) { - Log.d(TAG, "Failed to get cas@1.0 service"); + static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { + synchronized (sHidlLock) { + if (sServiceHidl != null) { + return sServiceHidl; + } else { + try { + Log.d(TAG, "Trying to get cas@1.2 service"); + android.hardware.cas.V1_2.IMediaCasService serviceV12 = + android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/); + if (serviceV12 != null) { + sServiceHidl = serviceV12; + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + return sServiceHidl; } - - return null; + } catch (Exception eV1_2) { + Log.d(TAG, "Failed to get cas@1.2 service"); } - }; - static IMediaCasService getService() { - return sService.get(); - } + try { + Log.d(TAG, "Trying to get cas@1.1 service"); + android.hardware.cas.V1_1.IMediaCasService serviceV11 = + android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/); + if (serviceV11 != null) { + sServiceHidl = serviceV11; + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + return sServiceHidl; + } + } catch (Exception eV1_1) { + Log.d(TAG, "Failed to get cas@1.1 service"); + } - static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { - return sServiceHidl.get(); + try { + Log.d(TAG, "Trying to get cas@1.0 service"); + sServiceHidl = + android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); + if (sServiceHidl != null) { + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + } + return sServiceHidl; + } catch (Exception eV1_0) { + Log.d(TAG, "Failed to get cas@1.0 service"); + } + } + } + // Couldn't find an HIDL service, returning null. + return null; } private void validateInternalStates() { @@ -756,7 +792,7 @@ public final class MediaCas implements AutoCloseable { * @return Whether the specified CA system is supported on this device. */ public static boolean isSystemIdSupported(int CA_system_id) { - IMediaCasService service = sService.get(); + IMediaCasService service = getService(); if (service != null) { try { return service.isSystemIdSupported(CA_system_id); @@ -765,7 +801,7 @@ public final class MediaCas implements AutoCloseable { } } - android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl(); if (serviceHidl != null) { try { return serviceHidl.isSystemIdSupported(CA_system_id); @@ -781,7 +817,7 @@ public final class MediaCas implements AutoCloseable { * @return an array of descriptors for the available CA plugins. */ public static PluginDescriptor[] enumeratePlugins() { - IMediaCasService service = sService.get(); + IMediaCasService service = getService(); if (service != null) { try { AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins(); @@ -794,10 +830,11 @@ public final class MediaCas implements AutoCloseable { } return results; } catch (RemoteException e) { + Log.e(TAG, "Some exception while enumerating plugins"); } } - android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl(); if (serviceHidl != null) { try { ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins(); diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java index a53a8ce79354..e4eaaa317b3d 100644 --- a/media/java/android/media/audiopolicy/AudioMix.java +++ b/media/java/android/media/audiopolicy/AudioMix.java @@ -24,6 +24,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioSystem; @@ -67,12 +68,19 @@ public class AudioMix implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) final int mDeviceSystemType; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_* + // The (virtual) device ID that this AudioMix was registered for. This value is overwritten + // when registering this AudioMix with an AudioPolicy or attaching this AudioMix to an + // AudioPolicy to match the AudioPolicy attribution. Does not imply that it only modifies + // audio routing for this device ID. + private int mVirtualDeviceId; + /** * All parameters are guaranteed valid through the Builder. */ private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format, int routeFlags, int callbackFlags, - int deviceType, @Nullable String deviceAddress, IBinder token) { + int deviceType, @Nullable String deviceAddress, IBinder token, + int virtualDeviceId) { mRule = Objects.requireNonNull(rule); mFormat = Objects.requireNonNull(format); mRouteFlags = routeFlags; @@ -81,6 +89,7 @@ public class AudioMix implements Parcelable { mDeviceSystemType = deviceType; mDeviceAddress = (deviceAddress == null) ? new String("") : deviceAddress; mToken = token; + mVirtualDeviceId = virtualDeviceId; } // CALLBACK_FLAG_* values: keep in sync with AudioMix::kCbFlag* values defined @@ -269,6 +278,11 @@ public class AudioMix implements Parcelable { } /** @hide */ + public boolean matchesVirtualDeviceId(int deviceId) { + return mVirtualDeviceId == deviceId; + } + + /** @hide */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -311,6 +325,7 @@ public class AudioMix implements Parcelable { mFormat.writeToParcel(dest, flags); mRule.writeToParcel(dest, flags); dest.writeStrongBinder(mToken); + dest.writeInt(mVirtualDeviceId); } public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() { @@ -331,6 +346,7 @@ public class AudioMix implements Parcelable { mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p)); mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p)); mixBuilder.setToken(p.readStrongBinder()); + mixBuilder.setVirtualDeviceId(p.readInt()); return mixBuilder.build(); } @@ -339,6 +355,15 @@ public class AudioMix implements Parcelable { } }; + /** + * Updates the deviceId of the AudioMix to match with the AudioPolicy the mix is registered + * through. + * @hide + */ + public void setVirtualDeviceId(int virtualDeviceId) { + mVirtualDeviceId = virtualDeviceId; + } + /** @hide */ @IntDef(flag = true, value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } ) @@ -354,6 +379,7 @@ public class AudioMix implements Parcelable { private int mRouteFlags = 0; private int mCallbackFlags = 0; private IBinder mToken = null; + private int mVirtualDeviceId = Context.DEVICE_ID_DEFAULT; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_* private int mDeviceSystemType = AudioSystem.DEVICE_NONE; private String mDeviceAddress = null; @@ -404,6 +430,15 @@ public class AudioMix implements Parcelable { /** * @hide + * Only used by AudioMix internally. + */ + Builder setVirtualDeviceId(int virtualDeviceId) { + mVirtualDeviceId = virtualDeviceId; + return this; + } + + /** + * @hide * Only used by AudioPolicyConfig, not a public API. * @param callbackFlags which callbacks are called from native * @return the same Builder instance. @@ -570,7 +605,7 @@ public class AudioMix implements Parcelable { } return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType, - mDeviceAddress, mToken); + mDeviceAddress, mToken, mVirtualDeviceId); } private int getLoopbackDeviceSystemTypeForAudioMixingRule(AudioMixingRule rule) { diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index 508c0a2b9a21..293a8f89fbca 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -27,6 +27,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.ActivityManager; +import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioAttributes; @@ -146,6 +147,16 @@ public class AudioPolicy { return mProjection; } + /** @hide */ + public AttributionSource getAttributionSource() { + return getAttributionSource(mContext); + } + + private static AttributionSource getAttributionSource(Context context) { + return context == null + ? AttributionSource.myAttributionSource() : context.getAttributionSource(); + } + /** * The parameters are guaranteed non-null through the Builder */ @@ -208,6 +219,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix argument"); } + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } mMixes.add(mix); return this; } @@ -358,6 +372,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix in attachMixes"); } else { + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } zeMixes.add(mix); } } @@ -400,6 +417,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix in detachMixes"); } else { + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } zeMixes.add(mix); } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index 32795e4b2f1c..e48c0f42e62e 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -96,6 +96,7 @@ class InstallRepository(private val context: Context) { var stagedSessionId = SessionInfo.INVALID_ID private set private var callingUid = Process.INVALID_UID + private var originatingUid = Process.INVALID_UID private var callingPackage: String? = null private var sessionStager: SessionStager? = null private lateinit var intent: Intent @@ -148,7 +149,7 @@ class InstallRepository(private val context: Context) { } val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage) // Uid of the source package, with a preference to uid from ApplicationInfo - val originatingUid = sourceInfo?.uid ?: callingUid + originatingUid = sourceInfo?.uid ?: callingUid appOpRequestInfo = AppOpRequestInfo( getPackageNameForUid(context, originatingUid, callingPackage), originatingUid, callingAttributionTag @@ -282,7 +283,7 @@ class InstallRepository(private val context: Context) { context.contentResolver.openAssetFileDescriptor(uri, "r").use { afd -> val pfd: ParcelFileDescriptor? = afd?.parcelFileDescriptor val params: SessionParams = - createSessionParams(intent, pfd, uri.toString()) + createSessionParams(originatingUid, intent, pfd, uri.toString()) stagedSessionId = packageInstaller.createSession(params) } } catch (e: Exception) { @@ -338,6 +339,7 @@ class InstallRepository(private val context: Context) { } private fun createSessionParams( + originatingUid: Int, intent: Intent, pfd: ParcelFileDescriptor?, debugPathName: String, @@ -354,9 +356,7 @@ class InstallRepository(private val context: Context) { params.setOriginatingUri( intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri::class.java) ) - params.setOriginatingUid( - intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID) - ) + params.setOriginatingUid(originatingUid) params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME)) params.setInstallReason(PackageManager.INSTALL_REASON_USER) // Disable full screen intent usage by for sideloads. diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp index 6dc07b29a510..4aa67c17ad98 100644 --- a/packages/SettingsLib/ProfileSelector/Android.bp +++ b/packages/SettingsLib/ProfileSelector/Android.bp @@ -20,6 +20,7 @@ android_library { static_libs: [ "com.google.android.material_material", "SettingsLibSettingsTheme", + "android.os.flags-aconfig-java-export", ], sdk_version: "system_current", diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml index 80f6b7683269..303e20c2497e 100644 --- a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml +++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml @@ -18,5 +18,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.widget.profileselector"> - <uses-sdk android:minSdkVersion="23" /> + <uses-sdk android:minSdkVersion="29" /> </manifest> diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml index 68d4047a497c..76ccb651969b 100644 --- a/packages/SettingsLib/ProfileSelector/res/values/strings.xml +++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml @@ -21,4 +21,6 @@ <string name="settingslib_category_personal">Personal</string> <!-- Header for items under the work user [CHAR LIMIT=30] --> <string name="settingslib_category_work">Work</string> + <!-- Header for items under the private profile user [CHAR LIMIT=30] --> + <string name="settingslib_category_private">Private</string> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java index be5753beea4e..c52386bef07b 100644 --- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java @@ -16,31 +16,77 @@ package com.android.settingslib.widget; +import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; +import android.content.pm.UserProperties; +import android.os.Build; import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.ArrayMap; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.core.os.BuildCompat; import androidx.fragment.app.Fragment; import androidx.viewpager2.widget.ViewPager2; +import com.android.settingslib.widget.profileselector.R; + import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; -import com.android.settingslib.widget.profileselector.R; + +import java.util.ArrayList; +import java.util.List; /** * Base fragment class for profile settings. */ public abstract class ProfileSelectFragment extends Fragment { + private static final String TAG = "ProfileSelectFragment"; + // UserHandle#USER_NULL is a @TestApi so is not accessible. + private static final int USER_NULL = -10000; + private static final int DEFAULT_POSITION = 0; + + /** + * The type of profile tab of {@link ProfileSelectFragment} to show + * <ul> + * <li>0: Personal tab. + * <li>1: Work profile tab. + * </ul> + * + * <p> Please note that this is supported for legacy reasons. Please use + * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} instead. + */ + public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab"; + + /** + * An {@link ArrayList} of users to show. The supported users are: System user, the managed + * profile user, and the private profile user. A client should pass all the user ids that need + * to be shown in this list. Note that if this list is not provided then, for legacy reasons + * see {@link #EXTRA_SHOW_FRAGMENT_TAB}, an attempt will be made to show two tabs: one for the + * System user and one for the managed profile user. + * + * <p>Please note that this MUST be used in conjunction with + * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} + */ + public static final String EXTRA_LIST_OF_USER_IDS = ":settings:list_user_ids"; /** - * Personal or Work profile tab of {@link ProfileSelectFragment} - * <p>0: Personal tab. - * <p>1: Work profile tab. + * The user id of the user to be show in {@link ProfileSelectFragment}. Only the below user + * types are supported: + * <ul> + * <li> System user. + * <li> Managed profile user. + * <li> Private profile user. + * </ul> + * + * <p>Please note that this MUST be used in conjunction with {@link #EXTRA_LIST_OF_USER_IDS}. */ - public static final String EXTRA_SHOW_FRAGMENT_TAB = - ":settings:show_fragment_tab"; + public static final String EXTRA_SHOW_FRAGMENT_USER_ID = ":settings:show_fragment_user_id"; /** * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB @@ -48,13 +94,23 @@ public abstract class ProfileSelectFragment extends Fragment { public static final int PERSONAL_TAB = 0; /** - * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB + * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB for the managed profile */ public static final int WORK_TAB = 1; + /** + * Please note that private profile is available from API LEVEL + * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} only, therefore PRIVATE_TAB MUST be + * passed in {@link #EXTRA_SHOW_FRAGMENT_TAB} and {@link #EXTRA_LIST_OF_PROFILE_TABS} for + * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher API Levels only. + */ + private static final int PRIVATE_TAB = 2; + private ViewGroup mContentView; private ViewPager2 mViewPager; + private final ArrayMap<UserHandle, Integer> mProfileTabsByUsers = new ArrayMap<>(); + private boolean mUsingUserIds = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -67,7 +123,7 @@ public abstract class ProfileSelectFragment extends Fragment { if (titleResId > 0) { activity.setTitle(titleResId); } - final int selectedTab = getTabId(activity, getArguments()); + initProfileTabsToShow(); final View tabContainer = mContentView.findViewById(R.id.tab_container); mViewPager = tabContainer.findViewById(R.id.view_pager); @@ -78,16 +134,14 @@ public abstract class ProfileSelectFragment extends Fragment { ).attach(); tabContainer.setVisibility(View.VISIBLE); - final TabLayout.Tab tab = tabs.getTabAt(selectedTab); + final TabLayout.Tab tab = tabs.getTabAt(getSelectedTabPosition(activity, getArguments())); tab.select(); return mContentView; } /** - * create Personal or Work profile fragment - * <p>0: Personal profile. - * <p>1: Work profile. + * Create Personal or Work or Private profile fragment. See {@link #EXTRA_SHOW_FRAGMENT_USER_ID} */ public abstract Fragment createFragment(int position); @@ -99,21 +153,90 @@ public abstract class ProfileSelectFragment extends Fragment { return 0; } - int getTabId(Activity activity, Bundle bundle) { + int getSelectedTabPosition(Activity activity, Bundle bundle) { if (bundle != null) { + final int extraUserId = bundle.getInt(EXTRA_SHOW_FRAGMENT_USER_ID, USER_NULL); + if (extraUserId != USER_NULL) { + return mProfileTabsByUsers.indexOfKey(UserHandle.of(extraUserId)); + } final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1); if (extraTab != -1) { return extraTab; } } - return PERSONAL_TAB; + return DEFAULT_POSITION; + } + + int getTabCount() { + return mUsingUserIds ? mProfileTabsByUsers.size() : 2; + } + + void initProfileTabsToShow() { + Bundle bundle = getArguments(); + if (bundle != null) { + ArrayList<Integer> userIdsToShow = + bundle.getIntegerArrayList(EXTRA_LIST_OF_USER_IDS); + if (userIdsToShow != null && !userIdsToShow.isEmpty()) { + mUsingUserIds = true; + UserManager userManager = getContext().getSystemService(UserManager.class); + List<UserHandle> userHandles = userManager.getUserProfiles(); + for (UserHandle userHandle : userHandles) { + if (!userIdsToShow.contains(userHandle.getIdentifier())) { + continue; + } + if (userHandle.isSystem()) { + mProfileTabsByUsers.put(userHandle, PERSONAL_TAB); + } else if (userManager.isManagedProfile(userHandle.getIdentifier())) { + mProfileTabsByUsers.put(userHandle, WORK_TAB); + } else if (shouldShowPrivateProfileIfItsOne(userHandle)) { + mProfileTabsByUsers.put(userHandle, PRIVATE_TAB); + } + } + } + } + } + + private int getProfileTabForPosition(int position) { + return mUsingUserIds ? mProfileTabsByUsers.valueAt(position) : position; + } + + int getUserIdForPosition(int position) { + return mUsingUserIds ? mProfileTabsByUsers.keyAt(position).getIdentifier() : position; } private CharSequence getPageTitle(int position) { - if (position == WORK_TAB) { + int tab = getProfileTabForPosition(position); + if (tab == WORK_TAB) { return getContext().getString(R.string.settingslib_category_work); + } else if (tab == PRIVATE_TAB) { + return getContext().getString(R.string.settingslib_category_private); } return getString(R.string.settingslib_category_personal); } + + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private boolean shouldShowUserInQuietMode(UserHandle userHandle, UserManager userManager) { + UserProperties userProperties = userManager.getUserProperties(userHandle); + return !userManager.isQuietModeEnabled(userHandle) + || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; + } + + // It's sufficient to have this method marked with the appropriate API level because we expect + // to be here only for this API level - when then private profile was introduced. + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private boolean shouldShowPrivateProfileIfItsOne(UserHandle userHandle) { + if (!BuildCompat.isAtLeastV() || !android.os.Flags.allowPrivateProfile()) { + return false; + } + try { + Context userContext = getContext().createContextAsUser(userHandle, /* flags= */ 0); + UserManager userManager = userContext.getSystemService(UserManager.class); + return userManager.isPrivateProfile() + && shouldShowUserInQuietMode(userHandle, userManager); + } catch (IllegalStateException exception) { + Log.i(TAG, "Ignoring this user as the calling package not available in this user."); + } + return false; + } } diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java index f5ab64742992..37f4f275cfe7 100644 --- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java @@ -18,7 +18,6 @@ package com.android.settingslib.widget; import androidx.fragment.app.Fragment; import androidx.viewpager2.adapter.FragmentStateAdapter; -import com.android.settingslib.widget.profileselector.R; /** * ViewPager Adapter to handle between TabLayout and ViewPager2 @@ -34,11 +33,11 @@ public class ProfileViewPagerAdapter extends FragmentStateAdapter { @Override public Fragment createFragment(int position) { - return mParentFragments.createFragment(position); + return mParentFragments.createFragment(mParentFragments.getUserIdForPosition(position)); } @Override public int getItemCount() { - return 2; + return mParentFragments.getTabCount(); } } diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 02d19dc84f2e..58040716db3e 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -932,6 +932,9 @@ <uses-permission android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" /> + <!-- Permission required for Cts test - CtsSettingsTestCases --> + <uses-permission android:name="android.permission.PREPARE_FACTORY_RESET" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java index 6546b87c8802..f70ad9ed58b0 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -23,10 +23,10 @@ import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_QU import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS; import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT; -import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU; +import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME; @@ -77,6 +77,8 @@ public class AccessibilityMenuServiceTest { private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5; private static final int TIMEOUT_UI_CHANGE_S = 5; private static final int NO_GLOBAL_ACTION = -1; + private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU) + .setPackage(PACKAGE_NAME); private static Instrumentation sInstrumentation; private static UiAutomation sUiAutomation; @@ -152,9 +154,6 @@ public class AccessibilityMenuServiceTest { @Before public void setup() throws Throwable { sOpenBlocked.set(false); - wakeUpScreen(); - sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); - openMenu(); } @After @@ -188,24 +187,17 @@ public class AccessibilityMenuServiceTest { } private static void openMenu() throws Throwable { - openMenu(false); - } - - private static void openMenu(boolean abandonOnBlock) throws Throwable { - Intent intent = new Intent(INTENT_TOGGLE_MENU); - intent.setPackage(PACKAGE_NAME); - sInstrumentation.getContext().sendBroadcast(intent); + unlockSignal(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); TestUtils.waitUntil("Timed out before menu could appear.", TIMEOUT_UI_CHANGE_S, () -> { - if (sOpenBlocked.get() && abandonOnBlock) { - throw new IllegalStateException(); - } if (isMenuVisible()) { return true; } else { - sInstrumentation.getContext().sendBroadcast(intent); + unlockSignal(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); return false; } }); @@ -249,6 +241,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAdjustBrightness() throws Throwable { + openMenu(); Context context = sInstrumentation.getTargetContext(); DisplayManager displayManager = context.getSystemService( DisplayManager.class); @@ -264,22 +257,28 @@ public class AccessibilityMenuServiceTest { context.getDisplayId()).getBrightnessInfo(); try { - displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum); TestUtils.waitUntil("Could not change to minimum brightness", TIMEOUT_UI_CHANGE_S, - () -> displayManager.getBrightness(context.getDisplayId()) - == brightnessInfo.brightnessMinimum); + () -> { + displayManager.setBrightness( + context.getDisplayId(), brightnessInfo.brightnessMinimum); + return displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMinimum; + }); brightnessUpButton.performAction(CLICK_ID); TestUtils.waitUntil("Did not detect an increase in brightness.", TIMEOUT_UI_CHANGE_S, () -> displayManager.getBrightness(context.getDisplayId()) > brightnessInfo.brightnessMinimum); - displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum); TestUtils.waitUntil("Could not change to maximum brightness", TIMEOUT_UI_CHANGE_S, - () -> displayManager.getBrightness(context.getDisplayId()) - == brightnessInfo.brightnessMaximum); + () -> { + displayManager.setBrightness( + context.getDisplayId(), brightnessInfo.brightnessMaximum); + return displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMaximum; + }); brightnessDownButton.performAction(CLICK_ID); TestUtils.waitUntil("Did not detect a decrease in brightness.", TIMEOUT_UI_CHANGE_S, @@ -292,6 +291,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAdjustVolume() throws Throwable { + openMenu(); Context context = sInstrumentation.getTargetContext(); AudioManager audioManager = context.getSystemService(AudioManager.class); int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); @@ -332,6 +332,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAssistantButton_opensVoiceAssistant() throws Throwable { + openMenu(); AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal())); Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND); @@ -349,6 +350,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable { + openMenu(); AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal())); Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); @@ -364,6 +366,7 @@ public class AccessibilityMenuServiceTest { @Test public void testPowerButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal())); @@ -376,6 +379,7 @@ public class AccessibilityMenuServiceTest { @Test public void testRecentButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal())); @@ -388,6 +392,7 @@ public class AccessibilityMenuServiceTest { @Test public void testLockButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal())); @@ -400,6 +405,7 @@ public class AccessibilityMenuServiceTest { @Test public void testQuickSettingsButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal())); @@ -412,6 +418,7 @@ public class AccessibilityMenuServiceTest { @Test public void testNotificationsButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal())); @@ -424,6 +431,7 @@ public class AccessibilityMenuServiceTest { @Test public void testScreenshotButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal())); @@ -436,6 +444,7 @@ public class AccessibilityMenuServiceTest { @Test public void testOnScreenLock_closesMenu() throws Throwable { + openMenu(); closeScreen(); wakeUpScreen(); @@ -447,13 +456,18 @@ public class AccessibilityMenuServiceTest { closeScreen(); wakeUpScreen(); - boolean blocked = false; - try { - openMenu(true); - } catch (IllegalStateException e) { - // Expected - blocked = true; - } - assertThat(blocked).isTrue(); + TestUtils.waitUntil("Did not receive signal that menu cannot open", + TIMEOUT_UI_CHANGE_S, + () -> { + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); + return sOpenBlocked.get(); + }); + } + + private static void unlockSignal() { + // MENU unlocks screen, + // BACK closes any menu that may appear if the screen wasn't locked. + sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); + sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK"); } } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8da50216f13c..ad09febd74a9 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -424,6 +424,13 @@ flag { } flag { + name: "screenshot_shelf_ui" + namespace: "systemui" + description: "Use new shelf UI flow for screenshots" + bug: "329659738" +} + +flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt index 82e19e7c154c..1d86b15dbf4f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt @@ -58,7 +58,6 @@ constructor( if (currentClock?.smallClock?.view == null) { return } - viewModel.clock = currentClock val context = LocalContext.current MovableElement(key = smallClockElementKey, modifier = modifier) { @@ -89,7 +88,6 @@ constructor( @Composable fun SceneScope.LargeClock(modifier: Modifier = Modifier) { val currentClock by viewModel.currentClock.collectAsState() - viewModel.clock = currentClock if (currentClock?.largeClock?.view == null) { return } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 5c9b271b342c..525ad161c94f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -16,45 +16,33 @@ package com.android.systemui.keyguard.ui.composable.section -import android.content.Context import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.notifications.ui.composable.NotificationStack import com.android.systemui.scene.shared.flag.SceneContainerFlags -import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher @SysUISingleton class NotificationSection @Inject constructor( - @Application private val context: Context, private val viewModel: NotificationsPlaceholderViewModel, - controller: NotificationStackScrollLayoutController, sceneContainerFlags: SceneContainerFlags, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, stackScrollLayout: NotificationStackScrollLayout, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main private val mainImmediateDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, + notificationStackViewBinder: NotificationStackViewBinder, ) { init { @@ -73,24 +61,13 @@ constructor( sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) } - SharedNotificationContainerBinder.bind( + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainImmediateDispatcher, ) if (sceneContainerFlags.isEnabled()) { - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainImmediateDispatcher, - ) + notificationStackViewBinder.bindWhileAttached() } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 8ad2bb78f5d6..9ba5e3b846ed 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize @@ -70,10 +71,10 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadi import com.android.systemui.notifications.ui.composable.Notifications.Form import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ui.composable.ShadeHeader import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt @@ -140,6 +141,7 @@ fun SceneScope.NotificationScrollingStack( ) { val density = LocalDensity.current val screenCornerRadius = LocalScreenCornerRadius.current + val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius) val scrollState = rememberScrollState() val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f) val expansionFraction by viewModel.expandFraction.collectAsState(0f) @@ -157,7 +159,7 @@ fun SceneScope.NotificationScrollingStack( val contentHeight = viewModel.intrinsicContentHeight.collectAsState() - val stackRounding = viewModel.stackRounding.collectAsState() + val stackRounding = viewModel.stackRounding.collectAsState(StackRounding()) // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is // calculated in minScrimOffset. The scrim is the same height as the screen minus the @@ -225,6 +227,7 @@ fun SceneScope.NotificationScrollingStack( .graphicsLayer { shape = calculateCornerRadius( + scrimCornerRadius, screenCornerRadius, { expansionFraction }, layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) @@ -357,6 +360,7 @@ private fun SceneScope.NotificationPlaceholder( } private fun calculateCornerRadius( + scrimCornerRadius: Dp, screenCornerRadius: Dp, expansionFraction: () -> Float, transitioning: Boolean, @@ -364,12 +368,12 @@ private fun calculateCornerRadius( return if (transitioning) { lerp( start = screenCornerRadius.value, - stop = SCRIM_CORNER_RADIUS, + stop = scrimCornerRadius.value, fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f), ) .dp } else { - SCRIM_CORNER_RADIUS.dp + scrimCornerRadius } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 2c31f9b6b92d..85798acd0dcd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -370,7 +370,8 @@ private fun SceneScope.SplitShade( NotificationScrollingStack( viewModel = viewModel.notifications, maxScrimTop = { 0f }, - modifier = Modifier.weight(1f).fillMaxHeight(), + modifier = + Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight), ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt index 6dd425c2afbc..e3fa89c5760d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt @@ -20,9 +20,12 @@ 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.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds +import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test @@ -30,10 +33,9 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class NotificationStackAppearanceInteractorTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val underTest = kosmos.notificationStackAppearanceInteractor @@ -59,6 +61,18 @@ class NotificationStackAppearanceInteractorTest : SysuiTestCase() { assertThat(stackBounds).isEqualTo(bounds2) } + @Test + fun stackRounding() = + testScope.runTest { + val stackRounding by collectLastValue(underTest.stackRounding) + + kosmos.shadeRepository.setShadeMode(ShadeMode.Single) + assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = false)) + + kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = true)) + } + @Test(expected = IllegalStateException::class) fun setStackBounds_withImproperBounds_throwsException() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt new file mode 100644 index 000000000000..a5ad3c325e51 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.DisposableHandle +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DisposableHandlesTest : SysuiTestCase() { + @Test + fun disposeWorksOnce() { + var handleDisposeCount = 0 + val underTest = DisposableHandles() + + // Add a handle + underTest += DisposableHandle { handleDisposeCount++ } + + // dispose() calls dispose() on children + underTest.dispose() + assertThat(handleDisposeCount).isEqualTo(1) + + // Once disposed, children are not disposed again + underTest.dispose() + assertThat(handleDisposeCount).isEqualTo(1) + } + + @Test + fun replaceCallsDispose() { + var handleDisposeCount1 = 0 + var handleDisposeCount2 = 0 + val underTest = DisposableHandles() + val handle1 = DisposableHandle { handleDisposeCount1++ } + val handle2 = DisposableHandle { handleDisposeCount2++ } + + // First add handle1 + underTest += handle1 + + // replace() calls dispose() on existing children + underTest.replaceAll(handle2) + assertThat(handleDisposeCount1).isEqualTo(1) + assertThat(handleDisposeCount2).isEqualTo(0) + + // Once disposed, replaced children are not disposed again + underTest.dispose() + assertThat(handleDisposeCount1).isEqualTo(1) + assertThat(handleDisposeCount2).isEqualTo(1) + } +} diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml new file mode 100644 index 000000000000..ef1a21f2fdf6 --- /dev/null +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -0,0 +1,160 @@ +<?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. + --> +<com.android.systemui.screenshot.ui.ScreenshotShelfView + 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" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <ImageView + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toTopOf="@id/guideline"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> + <LinearLayout + android:id="@+id/screenshot_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_share_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_edit_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_scroll_chip" + android:visibility="gone" /> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/screenshot_preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="14dp" + android:elevation="8dp" + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintBottom_toBottomOf="parent"/> + <ImageView + android:id="@+id/screenshot_preview" + android:layout_width="@dimen/overlay_x_scale" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" + android:elevation="8dp" + android:contentDescription="@string/screenshot_edit_description" + android:scaleType="fitEnd" + android:background="@drawable/overlay_preview_background" + android:adjustViewBounds="true" + android:clickable="true" + app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/> + <ImageView + android:id="@+id/screenshot_badge" + android:layout_width="56dp" + android:layout_height="56dp" + android:visibility="gone" + android:elevation="9dp" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> + <FrameLayout + android:id="@+id/screenshot_dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + android:elevation="11dp" + android:visibility="gone" + app:layout_constraintStart_toEndOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintBottom_toTopOf="@id/screenshot_preview" + android:contentDescription="@string/screenshot_dismiss_description"> + <ImageView + android:id="@+id/screenshot_dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:background="@drawable/circular_background" + android:backgroundTint="?androidprv:attr/materialColorPrimary" + android:tint="?androidprv:attr/materialColorOnPrimary" + android:padding="4dp" + android:src="@drawable/ic_close"/> + </FrameLayout> + <ImageView + android:id="@+id/screenshot_scrollable_preview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="matrix" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + android:elevation="7dp"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="0dp" /> + + <FrameLayout + android:id="@+id/screenshot_message_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginTop="4dp" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + android:paddingHorizontal="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/guideline" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintWidth_max="450dp" + app:layout_constraintHorizontal_bias="0"> + <include layout="@layout/screenshot_work_profile_first_run" /> + <include layout="@layout/screenshot_detection_notice" /> + </FrameLayout> +</com.android.systemui.screenshot.ui.ScreenshotShelfView> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index bf5eeb9e8294..3029888c7e54 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -235,6 +235,8 @@ <string name="screenshot_edit_label">Edit</string> <!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_edit_description">Edit screenshot</string> + <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] --> + <string name="screenshot_share_label">Share</string> <!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_share_description">Share screenshot</string> <!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] --> @@ -2011,6 +2013,10 @@ <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string> <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] --> <string name="system_multitasking_full_screen">Switch from split screen to full screen</string> + <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] --> + <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string> + <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] --> + <string name="system_multitasking_splitscreen_focus_lhs">Switch to app on left or above while using split screen</string> <!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] --> <string name="system_multitasking_replace">During split screen: replace an app from one to another</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index e621ffe4cbc4..86f64f809e42 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -631,7 +631,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS @Nullable public ClockController getClock() { if (migrateClocksToBlueprint()) { - return mKeyguardClockInteractor.getClock(); + return mKeyguardClockInteractor.getCurrentClock().getValue(); } else { return mClockEventController.getClock(); } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt index 964eb6f3a613..578389b57a99 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt @@ -54,6 +54,18 @@ constructor( } /** + * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device + * configuration. + * + * @see android.content.res.Resources.getDimensionPixelSize + */ + fun getDimensionPixelOffset(@DimenRes id: Int): Flow<Int> { + return configurationController.onDensityOrFontScaleChanged.emitOnStart().map { + context.resources.getDimensionPixelOffset(id) + } + } + + /** * Returns a [Flow] that emits a color that is kept in sync with the device theme. * * @see Utils.getColorAttrDefaultColor diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index 7ad5aac63837..7a56554be1d2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -113,7 +113,10 @@ constructor( override val currentClock: StateFlow<ClockController?> = currentClockId - .map { clockRegistry.createCurrentClock() } + .map { + clockEventController.clock = clockRegistry.createCurrentClock() + clockEventController.clock + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 4812e03ec3f6..89148b09b3ed 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -213,9 +213,10 @@ constructor( cs: ConstraintSet, viewModel: KeyguardClockViewModel ) { - if (!DEBUG || viewModel.clock == null) return + val currentClock = viewModel.currentClock.value + if (!DEBUG || currentClock == null) return val smallClockViewId = R.id.lockscreen_clock_view - val largeClockViewId = viewModel.clock!!.largeClock.layout.views[0].id + val largeClockViewId = currentClock.largeClock.layout.views[0].id Log.i( TAG, "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 01596ed2e3ef..fa1fe5ec1fe8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.binder import android.transition.TransitionManager import android.transition.TransitionSet import android.view.View.INVISIBLE +import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout @@ -40,7 +41,8 @@ import kotlinx.coroutines.launch object KeyguardClockViewBinder { private val TAG = KeyguardClockViewBinder::class.simpleName!! - + // When changing to new clock, we need to remove old clock views from burnInLayer + private var lastClock: ClockController? = null @JvmStatic fun bind( clockSection: ClockSection, @@ -55,12 +57,11 @@ object KeyguardClockViewBinder { } } keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { if (!migrateClocksToBlueprint()) return@launch viewModel.currentClock.collect { currentClock -> - cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer) - viewModel.clock = currentClock + cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer) addClockViews(currentClock, keyguardRootView) updateBurnInLayer(keyguardRootView, viewModel) applyConstraints(clockSection, keyguardRootView, true) @@ -76,7 +77,7 @@ object KeyguardClockViewBinder { launch { if (!migrateClocksToBlueprint()) return@launch viewModel.clockShouldBeCentered.collect { clockShouldBeCentered -> - viewModel.clock?.let { + viewModel.currentClock.value?.let { // Weather clock also has hasCustomPositionUpdatedAnimation as true // TODO(b/323020908): remove ID check if ( @@ -93,7 +94,7 @@ object KeyguardClockViewBinder { launch { if (!migrateClocksToBlueprint()) return@launch viewModel.isAodIconsVisible.collect { isAodIconsVisible -> - viewModel.clock?.let { + viewModel.currentClock.value?.let { // Weather clock also has hasCustomPositionUpdatedAnimation as true if ( viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER" @@ -132,11 +133,14 @@ object KeyguardClockViewBinder { } private fun cleanupClockViews( - clockController: ClockController?, + currentClock: ClockController?, rootView: ConstraintLayout, burnInLayer: Layer? ) { - clockController?.let { clock -> + if (lastClock == currentClock) { + return + } + lastClock?.let { clock -> clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) rootView.removeView(it) @@ -150,6 +154,7 @@ object KeyguardClockViewBinder { } clock.largeClock.layout.views.forEach { rootView.removeView(it) } } + lastClock = currentClock } @VisibleForTesting @@ -157,11 +162,19 @@ object KeyguardClockViewBinder { clockController: ClockController?, rootView: ConstraintLayout, ) { + // We'll collect the same clock when exiting wallpaper picker without changing clock + // so we need to remove clock views from parent before addView again clockController?.let { clock -> clock.smallClock.layout.views.forEach { + if (it.parent != null) { + (it.parent as ViewGroup).removeView(it) + } rootView.addView(it).apply { it.visibility = INVISIBLE } } clock.largeClock.layout.views.forEach { + if (it.parent != null) { + (it.parent as ViewGroup).removeView(it) + } rootView.addView(it).apply { it.visibility = INVISIBLE } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index b77f0c5a1e60..4d0a25fb7cd3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -41,7 +41,7 @@ object KeyguardSmartspaceViewBinder { blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { if (!migrateClocksToBlueprint()) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 7c76e6afc074..f60da0e842e8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.util.kotlin.DisposableHandles import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -173,7 +174,7 @@ constructor( private lateinit var smallClockHostView: FrameLayout private var smartSpaceView: View? = null - private val disposables = mutableSetOf<DisposableHandle>() + private val disposables = DisposableHandles() private var isDestroyed = false private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>() @@ -183,7 +184,7 @@ constructor( init { coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job()) - disposables.add(DisposableHandle { coroutineScope.cancel() }) + disposables += DisposableHandle { coroutineScope.cancel() } if (keyguardBottomAreaRefactor()) { quickAffordancesCombinedViewModel.enablePreviewMode( @@ -214,7 +215,7 @@ constructor( if (hostToken == null) null else InputTransferToken(hostToken), "KeyguardPreviewRenderer" ) - disposables.add(DisposableHandle { host.release() }) + disposables += DisposableHandle { host.release() } } } @@ -284,7 +285,7 @@ constructor( fun destroy() { isDestroyed = true lockscreenSmartspaceController.disconnect() - disposables.forEach { it.dispose() } + disposables.dispose() if (keyguardBottomAreaRefactor()) { shortcutsBindings.forEach { it.destroy() } } @@ -372,7 +373,7 @@ constructor( private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) if (!keyguardBottomAreaRefactor()) { - disposables.add( + disposables += KeyguardRootViewBinder.bind( keyguardRootView, keyguardRootViewModel, @@ -387,7 +388,6 @@ constructor( null, // device entry haptics not required for preview mode null, // falsing manager not required for preview mode ) - ) } rootView.addView( keyguardRootView, @@ -555,14 +555,12 @@ constructor( } } clockRegistry.registerClockChangeListener(clockChangeListener) - disposables.add( - DisposableHandle { - clockRegistry.unregisterClockChangeListener(clockChangeListener) - } - ) + disposables += DisposableHandle { + clockRegistry.unregisterClockChangeListener(clockChangeListener) + } clockController.registerListeners(parentView) - disposables.add(DisposableHandle { clockController.unregisterListeners() }) + disposables += DisposableHandle { clockController.unregisterListeners() } } val receiver = @@ -581,7 +579,7 @@ constructor( addAction(Intent.ACTION_TIME_CHANGED) }, ) - disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }) + disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) } if (!migrateClocksToBlueprint()) { val layoutChangeListener = @@ -602,9 +600,9 @@ constructor( } } parentView.addOnLayoutChangeListener(layoutChangeListener) - disposables.add( - DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) } - ) + disposables += DisposableHandle { + parentView.removeOnLayoutChangeListener(layoutChangeListener) + } } onClockChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt index 9c9df806c38c..a215efa724f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt @@ -41,7 +41,7 @@ class BaseBlueprintTransition(val clockViewModel: KeyguardClockViewModel) : Tran private fun excludeClockAndSmartspaceViews(transition: Transition) { transition.excludeTarget(SmartspaceView::class.java, true) - clockViewModel.clock?.let { clock -> + clockViewModel.currentClock.value?.let { clock -> clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) } clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt index 3adeb2aeb283..c69d868866d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt @@ -57,7 +57,9 @@ class IntraBlueprintTransition( when (config.type) { Type.NoTransition -> {} Type.DefaultClockStepping -> - addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) }) + addTransition( + clockViewModel.currentClock.value?.let { DefaultClockSteppingTransition(it) } + ) else -> addTransition(ClockSizeTransition(config, clockViewModel, smartspaceViewModel)) } } 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 a183b720c087..7847c1ce3968 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 @@ -86,7 +86,7 @@ constructor( if (!Flags.migrateClocksToBlueprint()) { return } - clockInteractor.clock?.let { clock -> + keyguardClockViewModel.currentClock.value?.let { clock -> constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet)) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 6a3b920f9692..c1b0cc6b6db9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -26,20 +26,16 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.centralizedStatusBarHeightFix import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import dagger.Lazy import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher /** Single column format for notifications (default for phones) */ class DefaultNotificationStackScrollLayoutSection @@ -50,12 +46,9 @@ constructor( notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, + notificationStackViewBinder: NotificationStackViewBinder, private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, - @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( context, @@ -63,11 +56,8 @@ constructor( notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, + notificationStackViewBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { if (!migrateClocksToBlueprint()) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index 5dea7cbb801d..83235020b416 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt @@ -31,16 +31,11 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DisposableHandle +import com.android.systemui.util.kotlin.DisposableHandles abstract class NotificationStackScrollLayoutSection constructor( @@ -49,14 +44,11 @@ constructor( private val notificationPanelView: NotificationPanelView, private val sharedNotificationContainer: SharedNotificationContainer, private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - private val ambientState: AmbientState, - private val controller: NotificationStackScrollLayoutController, - private val notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val mainDispatcher: CoroutineDispatcher, + private val sharedNotificationContainerBinder: SharedNotificationContainerBinder, + private val notificationStackViewBinder: NotificationStackViewBinder, ) : KeyguardSection() { private val placeHolderId = R.id.nssl_placeholder - private val disposableHandles: MutableList<DisposableHandle> = mutableListOf() + private val disposableHandles = DisposableHandles() /** * Align the notification placeholder bottom to the top of either the lock icon or the ambient @@ -102,39 +94,20 @@ constructor( return } - disposeHandles() - disposableHandles.add( - SharedNotificationContainerBinder.bind( + disposableHandles.dispose() + disposableHandles += + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainDispatcher, ) - ) if (sceneContainerFlags.isEnabled()) { - disposableHandles.add( - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainDispatcher, - ) - ) + disposableHandles += notificationStackViewBinder.bindWhileAttached() } } override fun removeViews(constraintLayout: ConstraintLayout) { - disposeHandles() + disposableHandles.dispose() constraintLayout.removeView(placeHolderId) } - - private fun disposeHandles() { - disposableHandles.forEach { it.dispose() } - disposableHandles.clear() - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt index 2545302ccaa1..4a705a7f849d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt @@ -24,19 +24,14 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher /** Large-screen format for notifications, shown as two columns on the device */ class SplitShadeNotificationStackScrollLayoutSection @@ -47,12 +42,8 @@ constructor( notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, - @Main mainDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, + notificationStackViewBinder: NotificationStackViewBinder, ) : NotificationStackScrollLayoutSection( context, @@ -60,11 +51,8 @@ constructor( notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, + notificationStackViewBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { if (!migrateClocksToBlueprint()) { 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 6184c82cbff7..4d3a78d32b3a 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 @@ -216,7 +216,9 @@ class ClockSizeTransition( captureSmartspace = !viewModel.useLargeClock && smartspaceViewModel.isSmartspaceEnabled if (viewModel.useLargeClock) { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + viewModel.currentClock.value?.let { + it.largeClock.layout.views.forEach { addTarget(it) } + } } else { addTarget(R.id.lockscreen_clock_view) } @@ -276,7 +278,9 @@ class ClockSizeTransition( if (viewModel.useLargeClock) { addTarget(R.id.lockscreen_clock_view) } else { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + viewModel.currentClock.value?.let { + it.largeClock.layout.views.forEach { addTarget(it) } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index f961e083e64f..9c1f0770708c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -169,7 +169,7 @@ constructor( provider: Provider<ClockController>?, ): Provider<ClockController>? { return if (Flags.migrateClocksToBlueprint()) { - Provider { keyguardClockViewModel.clock } + Provider { keyguardClockViewModel.currentClock.value } } else { provider } 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 b6622e5c07b1..1c1c33ab7e7e 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 @@ -26,7 +26,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.SettingsClockSize -import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode @@ -54,8 +53,6 @@ constructor( val useLargeClock: Boolean get() = clockSize.value == LARGE - var clock: ClockController? by keyguardClockInteractor::clock - val clockSize = combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) { selectedSize, diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 5d2aeef5eb16..b34b3701528b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -432,6 +432,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { for (int i = 0; i < NP; i++) { mPages.get(i).removeAllViews(); } + if (mPageIndicator != null) { + mPageIndicator.setNumPages(numPages); + } if (NP == numPages) { return; } @@ -443,7 +446,6 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mLogger.d("Removing page"); mPages.remove(mPages.size() - 1); } - mPageIndicator.setNumPages(mPages.size()); setAdapter(mAdapter); mAdapter.notifyDataSetChanged(); if (mPageToRestore != NO_PAGE) { diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt index 7313a49be1bf..832fc3f00022 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt @@ -17,6 +17,7 @@ package com.android.systemui.recordissue import android.annotation.SuppressLint +import android.app.AlertDialog import android.content.Context import android.content.res.ColorStateList import android.graphics.Color @@ -74,7 +75,6 @@ constructor( @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch private lateinit var issueTypeButton: Button - private var hasSelectedIssueType: Boolean = false @MainThread override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { @@ -86,15 +86,13 @@ constructor( setPositiveButton( R.string.qs_record_issue_start, { _, _ -> - if (hasSelectedIssueType) { - onStarted.accept( - IssueRecordingConfig( - screenRecordSwitch.isChecked, - true /* TODO: Base this on issueType selected */ - ) + onStarted.accept( + IssueRecordingConfig( + screenRecordSwitch.isChecked, + true /* TODO: Base this on issueType selected */ ) - dismiss() - } + ) + dismiss() }, false ) @@ -115,8 +113,12 @@ constructor( bgExecutor.execute { onScreenRecordSwitchClicked() } } } + val startButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) issueTypeButton = requireViewById(R.id.issue_type_button) - issueTypeButton.setOnClickListener { onIssueTypeClicked(context) } + issueTypeButton.setOnClickListener { + onIssueTypeClicked(context) { startButton.isEnabled = true } + } + startButton.isEnabled = false } } @@ -159,7 +161,7 @@ constructor( } @MainThread - private fun onIssueTypeClicked(context: Context) { + private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) { val selectedCategory = issueTypeButton.text.toString() val popupMenu = PopupMenu(context, issueTypeButton) @@ -174,11 +176,11 @@ constructor( popupMenu.apply { setOnMenuItemClickListener { issueTypeButton.text = it.title + onIssueTypeSelected.run() true } setForceShowIcon(true) show() } - hasSelectedIssueType = true } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt new file mode 100644 index 000000000000..abdbd6880b33 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.UserHandle +import androidx.appcompat.content.res.AppCompatResources +import com.android.systemui.res.R +import javax.inject.Inject + +/** + * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI + * implementation. + */ +interface ScreenshotActionsProvider { + data class ScreenshotAction( + val icon: Drawable?, + val text: String?, + val overrideTransition: Boolean, + val retrieveIntent: (Uri) -> Intent + ) + + fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent + fun getActions(context: Context, user: UserHandle): List<ScreenshotAction> +} + +class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider { + override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent { + return ActionIntentCreator.createEdit(uri, context) + } + + override fun getActions( + context: Context, + user: UserHandle + ): List<ScreenshotActionsProvider.ScreenshotAction> { + val editAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit), + context.resources.getString(R.string.screenshot_edit_label), + true + ) { uri -> + ActionIntentCreator.createEdit(uri, context) + } + val shareAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share), + context.resources.getString(R.string.screenshot_share_label), + false + ) { uri -> + ActionIntentCreator.createShare(uri) + } + return listOf(editAction, shareAction) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt new file mode 100644 index 000000000000..9354fd27ce5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -0,0 +1,226 @@ +/* + * 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.screenshot + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowInsets +import android.window.OnBackInvokedCallback +import android.window.OnBackInvokedDispatcher +import com.android.internal.logging.UiEventLogger +import com.android.systemui.log.DebugLogger.debugLog +import com.android.systemui.res.R +import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS +import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS +import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT +import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER +import com.android.systemui.screenshot.scroll.ScrollCaptureController +import com.android.systemui.screenshot.ui.ScreenshotAnimationController +import com.android.systemui.screenshot.ui.ScreenshotShelfView +import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Controls the screenshot view and viewModel. */ +class ScreenshotShelfViewProxy +@AssistedInject +constructor( + private val logger: UiEventLogger, + private val viewModel: ScreenshotViewModel, + private val staticActionsProvider: ScreenshotActionsProvider, + @Assisted private val context: Context, + @Assisted private val displayId: Int +) : ScreenshotViewProxy { + override val view: ScreenshotShelfView = + LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView + override val screenshotPreview: View + override var packageName: String = "" + override var callbacks: ScreenshotView.ScreenshotViewCallback? = null + override var screenshot: ScreenshotData? = null + set(value) { + viewModel.setScreenshotBitmap(value?.bitmap) + field = value + } + + override val isAttachedToWindow + get() = view.isAttachedToWindow + override var isDismissing = false + override var isPendingSharedTransition = false + + private val animationController = ScreenshotAnimationController(view) + + init { + ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) + addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" } + screenshotPreview = view.screenshotPreview + } + + override fun reset() { + animationController.cancel() + isPendingSharedTransition = false + viewModel.setScreenshotBitmap(null) + viewModel.setActions(listOf()) + } + override fun updateInsets(insets: WindowInsets) {} + override fun updateOrientation(insets: WindowInsets) {} + + override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator { + return animationController.getEntranceAnimation() + } + + override fun addQuickShareChip(quickShareAction: Notification.Action) {} + + override fun setChipIntents(imageData: ScreenshotController.SavedImageData) { + val staticActions = + staticActionsProvider.getActions(context, imageData.owner).map { + ActionButtonViewModel(it.icon, it.text) { + val intent = it.retrieveIntent(imageData.uri) + debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" } + isPendingSharedTransition = true + callbacks?.onAction(intent, imageData.owner, it.overrideTransition) + } + } + + viewModel.setActions(staticActions) + } + + override fun requestDismissal(event: ScreenshotEvent) { + debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" } + + // If we're already animating out, don't restart the animation + if (isDismissing) { + debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" } + return + } + logger.log(event, 0, packageName) + val animator = animationController.getExitAnimation() + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + isDismissing = true + } + override fun onAnimationEnd(animator: Animator) { + isDismissing = false + callbacks?.onDismiss() + } + } + ) + animator.start() + } + + override fun showScrollChip(packageName: String, onClick: Runnable) {} + + override fun hideScrollChip() {} + + override fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, + ) {} + + override fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) {} + + override fun restoreNonScrollingUi() {} + + override fun stopInputListening() {} + + override fun requestFocus() { + view.requestFocus() + } + + override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) + + override fun prepareEntranceAnimation(runnable: Runnable) { + view.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" } + view.viewTreeObserver.removeOnPreDrawListener(this) + runnable.run() + return true + } + } + ) + } + + private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + val onBackInvokedCallback = OnBackInvokedCallback { + debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + } + view.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + onBackInvokedCallback + ) + } + + override fun onViewDetachedFromWindow(view: View) { + debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) + } + } + ) + } + private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + view.setOnKeyListener( + object : View.OnKeyListener { + override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + return true + } + return false + } + } + ) + } + + @AssistedFactory + interface Factory : ScreenshotViewProxy.Factory { + override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index cdb9abb15e84..9118ee1dfc73 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -16,16 +16,23 @@ package com.android.systemui.screenshot.dagger; +import static com.android.systemui.Flags.screenshotShelfUi; + import android.app.Service; +import android.view.accessibility.AccessibilityManager; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.screenshot.DefaultScreenshotActionsProvider; import com.android.systemui.screenshot.ImageCapture; import com.android.systemui.screenshot.ImageCaptureImpl; import com.android.systemui.screenshot.LegacyScreenshotViewProxy; import com.android.systemui.screenshot.RequestProcessor; +import com.android.systemui.screenshot.ScreenshotActionsProvider; import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; import com.android.systemui.screenshot.ScreenshotProxyService; import com.android.systemui.screenshot.ScreenshotRequestProcessor; +import com.android.systemui.screenshot.ScreenshotShelfViewProxy; import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; @@ -34,6 +41,7 @@ import com.android.systemui.screenshot.ScreenshotViewProxy; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel; import dagger.Binds; import dagger.Module; @@ -85,9 +93,25 @@ public abstract class ScreenshotModule { abstract ScreenshotSoundController bindScreenshotSoundController( ScreenshotSoundControllerImpl screenshotSoundProviderImpl); + @Binds + abstract ScreenshotActionsProvider bindScreenshotActionsProvider( + DefaultScreenshotActionsProvider defaultScreenshotActionsProvider); + + @Provides + @SysUISingleton + static ScreenshotViewModel providesScreenshotViewModel( + AccessibilityManager accessibilityManager) { + return new ScreenshotViewModel(accessibilityManager); + } + @Provides static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory( + ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory, LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) { - return legacyScreenshotViewProxyFactory; + if (screenshotShelfUi()) { + return shelfScreenshotViewProxyFactory; + } else { + return legacyScreenshotViewProxyFactory; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt new file mode 100644 index 000000000000..2c178736d9c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.view.View + +class ScreenshotAnimationController(private val view: View) { + private var animator: Animator? = null + + fun getEntranceAnimation(): Animator { + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.addUpdateListener { view.alpha = it.animatedFraction } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 0f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 1f + } + } + ) + this.animator = animator + return animator + } + + fun getExitAnimation(): Animator { + val animator = ValueAnimator.ofFloat(1f, 0f) + animator.addUpdateListener { view.alpha = it.animatedValue as Float } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 1f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 0f + } + } + ) + this.animator = animator + return animator + } + + fun cancel() { + animator?.cancel() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt new file mode 100644 index 000000000000..747ad4f9e48c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.systemui.res.R + +class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs) { + lateinit var screenshotPreview: ImageView + + override fun onFinishInflate() { + super.onFinishInflate() + screenshotPreview = requireViewById(R.id.screenshot_preview) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt new file mode 100644 index 000000000000..a5825b5f7797 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.binder + +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel + +object ActionButtonViewBinder { + /** Binds the given view to the given view-model */ + fun bind(view: View, viewModel: ActionButtonViewModel) { + val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon) + val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text) + iconView.setImageDrawable(viewModel.icon) + textView.text = viewModel.name + setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false) + if (viewModel.onClicked != null) { + view.setOnClickListener { viewModel.onClicked.invoke() } + } else { + view.setOnClickListener(null) + } + view.visibility = View.VISIBLE + view.alpha = 1f + } + + private fun setMargins(iconView: View, textView: View, hasText: Boolean) { + val iconParams = iconView.layoutParams as LinearLayout.LayoutParams + val textParams = textView.layoutParams as LinearLayout.LayoutParams + if (hasText) { + iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start) + iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing) + textParams.marginStart = 0 + textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end) + } else { + val paddingHorizontal = + iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal) + iconParams.marginStart = paddingHorizontal + iconParams.marginEnd = paddingHorizontal + } + iconView.layoutParams = iconParams + textView.layoutParams = textParams + } + + private fun View.dpToPx(dimenId: Int): Int { + return this.resources.getDimensionPixelSize(dimenId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt new file mode 100644 index 000000000000..3bcd52cbc99e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt @@ -0,0 +1,90 @@ +/* + * 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.screenshot.ui.binder + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import com.android.systemui.util.children +import kotlinx.coroutines.launch + +object ScreenshotShelfViewBinder { + fun bind( + view: ViewGroup, + viewModel: ScreenshotViewModel, + layoutInflater: LayoutInflater, + ) { + val previewView: ImageView = view.requireViewById(R.id.screenshot_preview) + val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border) + previewView.clipToOutline = true + val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions) + view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility = + if (viewModel.showDismissButton) View.VISIBLE else View.GONE + + view.repeatWhenAttached { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.preview.collect { bitmap -> + if (bitmap != null) { + previewView.setImageBitmap(bitmap) + previewView.visibility = View.VISIBLE + previewBorder.visibility = View.VISIBLE + } else { + previewView.visibility = View.GONE + previewBorder.visibility = View.GONE + } + } + } + launch { + viewModel.actions.collect { actions -> + if (actions.isNotEmpty()) { + view + .requireViewById<View>(R.id.actions_container_background) + .visibility = View.VISIBLE + } + val viewPool = actionsContainer.children.toList() + actionsContainer.removeAllViews() + val actionButtons = + List(actions.size) { + viewPool.getOrElse(it) { + layoutInflater.inflate( + R.layout.overlay_action_chip, + actionsContainer, + false + ) + } + } + actionButtons.zip(actions).forEach { + actionsContainer.addView(it.first) + ActionButtonViewBinder.bind(it.first, it.second) + } + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt new file mode 100644 index 000000000000..6ee970534352 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.viewmodel + +import android.graphics.drawable.Drawable + +data class ActionButtonViewModel( + val icon: Drawable?, + val name: String?, + val onClicked: (() -> Unit)? +) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt new file mode 100644 index 000000000000..3a652d90bb78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.viewmodel + +import android.graphics.Bitmap +import android.view.accessibility.AccessibilityManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) { + private val _preview = MutableStateFlow<Bitmap?>(null) + val preview: StateFlow<Bitmap?> = _preview + private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>()) + val actions: StateFlow<List<ActionButtonViewModel>> = _actions + val showDismissButton: Boolean + get() = accessibilityManager.isEnabled + + fun setScreenshotBitmap(bitmap: Bitmap?) { + _preview.value = bitmap + } + + fun setActions(actions: List<ActionButtonViewModel>) { + _actions.value = actions + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index da8c1bebce92..d6858cad6d0b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -631,34 +631,41 @@ public final class KeyboardShortcutListSearch { // Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow // Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow // Switch from Split screen to full screen: Meta + Ctrl + Up arrow - String[] shortcutLabels = { - context.getString(R.string.system_multitasking_rhs), - context.getString(R.string.system_multitasking_lhs), - context.getString(R.string.system_multitasking_full_screen), - }; - int[] keyCodes = { - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_UP, - }; - - for (int i = 0; i < shortcutLabels.length; i++) { - List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup( - new KeyboardShortcutInfo( - shortcutLabels[i], - keyCodes[i], - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON), - null)); - ShortcutMultiMappingInfo shortcutMultiMappingInfo = - new ShortcutMultiMappingInfo( - shortcutLabels[i], - null, - shortcutKeyGroups); - systemMultitaskingGroup.addItem(shortcutMultiMappingInfo); - } + // Change split screen focus to RHS: Meta + Alt + Right arrow + // Change split screen focus to LHS: Meta + Alt + Left arrow + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_rhs), + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_lhs), + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_full_screen), + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut( + context.getString(R.string.system_multitasking_splitscreen_focus_rhs), + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut( + context.getString(R.string.system_multitasking_splitscreen_focus_lhs), + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON)); return systemMultitaskingGroup; } + private static ShortcutMultiMappingInfo getMultitaskingShortcut(String shortcutLabel, + int keycode, int modifiers) { + List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList( + new ShortcutKeyGroup(new KeyboardShortcutInfo(shortcutLabel, keycode, modifiers), + null)); + return new ShortcutMultiMappingInfo(shortcutLabel, null, shortcutKeyGroups); + } + private static KeyboardShortcutMultiMappingGroup getMultiMappingInputShortcuts( Context context) { List<ShortcutMultiMappingInfo> shortcutMultiMappingInfoList = Arrays.asList( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt index 9fffb66ac831..79ba25e1e23e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.stack.data.repository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds -import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -29,10 +28,6 @@ class NotificationStackAppearanceRepository @Inject constructor() { /** The bounds of the notification stack in the current scene. */ val stackBounds = MutableStateFlow(StackBounds()) - /** The whether the corners of the notification stack should be rounded */ - // TODO: replace with the logic from QSController - val stackRounding = MutableStateFlow(StackRounding(roundTop = true, roundBottom = false)) - /** * The height in px of the contents of notification stack. Depending on the number of * notifications, this can exceed the space available on screen to show notifications, at which diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 5a56ca1444dc..f05d01717a44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding @@ -25,6 +27,9 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf /** An interactor which controls the appearance of the NSSL */ @SysUISingleton @@ -32,12 +37,30 @@ class NotificationStackAppearanceInteractor @Inject constructor( private val repository: NotificationStackAppearanceRepository, + shadeInteractor: ShadeInteractor, ) { /** The bounds of the notification stack in the current scene. */ val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow() + /** + * Whether the stack is expanding from GONE-with-HUN to SHADE + * + * TODO(b/296118689): implement this to match legacy QSController logic + */ + private val isExpandingFromHeadsUp: Flow<Boolean> = flowOf(false) + /** The rounding of the notification stack. */ - val stackRounding: StateFlow<StackRounding> = repository.stackRounding.asStateFlow() + val stackRounding: Flow<StackRounding> = + combine( + shadeInteractor.shadeMode, + isExpandingFromHeadsUp, + ) { shadeMode, isExpandingFromHeadsUp -> + StackRounding( + roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), + roundBottom = shadeMode != ShadeMode.Single, + ) + } + .distinctUntilChanged() /** * The height in px of the contents of notification stack. Depending on the number of diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt deleted file mode 100644 index 189c5e03ce07..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.stack.ui.viewbinder - -import android.content.Context -import android.util.TypedValue -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel -import kotlin.math.roundToInt -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.launch - -/** Binds the shared notification container to its view-model. */ -object NotificationStackAppearanceViewBinder { - const val SCRIM_CORNER_RADIUS = 32f - - @JvmStatic - fun bind( - context: Context, - view: SharedNotificationContainer, - viewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - @Main mainImmediateDispatcher: CoroutineDispatcher, - ): DisposableHandle { - return view.repeatWhenAttached(mainImmediateDispatcher) { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.stackClipping.collect { (bounds, rounding) -> - val viewLeft = controller.view.left - val viewTop = controller.view.top - val roundRadius = SCRIM_CORNER_RADIUS.dpToPx(context) - controller.setRoundedClippingBounds( - bounds.left.roundToInt() - viewLeft, - bounds.top.roundToInt() - viewTop, - bounds.right.roundToInt() - viewLeft, - bounds.bottom.roundToInt() - viewTop, - if (rounding.roundTop) roundRadius else 0, - if (rounding.roundBottom) roundRadius else 0, - ) - } - } - - launch { - viewModel.contentTop.collect { - controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) - } - } - - launch { - var wasExpanding = false - viewModel.expandFraction.collect { expandFraction -> - val nowExpanding = expandFraction != 0f && expandFraction != 1f - if (nowExpanding && !wasExpanding) { - controller.onExpansionStarted() - } - ambientState.expansionFraction = expandFraction - controller.expandedHeight = expandFraction * controller.view.height - if (!nowExpanding && wasExpanding) { - controller.onExpansionStopped() - } - wasExpanding = nowExpanding - } - } - - launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } - } - } - } - - private fun Float.dpToPx(context: Context): Int { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - this, - context.resources.displayMetrics - ) - .roundToInt() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt new file mode 100644 index 000000000000..1a34bb4f02c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.ui.viewbinder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** Binds the NSSL/Controller/AmbientState to their ViewModel. */ +@SysUISingleton +class NotificationStackViewBinder +@Inject +constructor( + @Main private val mainImmediateDispatcher: CoroutineDispatcher, + private val ambientState: AmbientState, + private val view: NotificationStackScrollLayout, + private val controller: NotificationStackScrollLayoutController, + private val viewModel: NotificationStackAppearanceViewModel, + private val configuration: ConfigurationState, +) { + + fun bindWhileAttached(): DisposableHandle { + return view.repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { bind() } + } + } + + suspend fun bind() = coroutineScope { + launch { + combine(viewModel.stackClipping, clipRadius, ::Pair).collect { (clipping, clipRadius) -> + val (bounds, rounding) = clipping + val viewLeft = controller.view.left + val viewTop = controller.view.top + controller.setRoundedClippingBounds( + bounds.left.roundToInt() - viewLeft, + bounds.top.roundToInt() - viewTop, + bounds.right.roundToInt() - viewLeft, + bounds.bottom.roundToInt() - viewTop, + if (rounding.roundTop) clipRadius else 0, + if (rounding.roundBottom) clipRadius else 0, + ) + } + } + + launch { + viewModel.contentTop.collect { + controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) + } + } + + launch { + var wasExpanding = false + viewModel.expandFraction.collect { expandFraction -> + val nowExpanding = expandFraction != 0f && expandFraction != 1f + if (nowExpanding && !wasExpanding) { + controller.onExpansionStarted() + } + ambientState.expansionFraction = expandFraction + controller.expandedHeight = expandFraction * controller.view.height + if (!nowExpanding && wasExpanding) { + controller.onExpansionStopped() + } + wasExpanding = nowExpanding + } + } + + launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } + } + + private val clipRadius: Flow<Int> + get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 7c76ddbec105..6db6719c76c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.WindowInsets import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor @@ -30,6 +31,8 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel +import com.android.systemui.util.kotlin.DisposableHandles +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.MutableStateFlow @@ -38,18 +41,23 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** Binds the shared notification container to its view-model. */ -object SharedNotificationContainerBinder { +@SysUISingleton +class SharedNotificationContainerBinder +@Inject +constructor( + private val sceneContainerFlags: SceneContainerFlags, + private val controller: NotificationStackScrollLayoutController, + private val notificationStackSizeCalculator: NotificationStackSizeCalculator, + @Main private val mainImmediateDispatcher: CoroutineDispatcher, +) { - @JvmStatic fun bind( view: SharedNotificationContainer, viewModel: SharedNotificationContainerViewModel, - sceneContainerFlags: SceneContainerFlags, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main mainImmediateDispatcher: CoroutineDispatcher, ): DisposableHandle { - val disposableHandle = + val disposables = DisposableHandles() + + disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { @@ -72,24 +80,6 @@ object SharedNotificationContainerBinder { } } - // Required to capture keyguard media changes and ensure the notification count is correct - val layoutChangeListener = - object : View.OnLayoutChangeListener { - override fun onLayoutChange( - view: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - viewModel.notificationStackChanged() - } - } - val burnInParams = MutableStateFlow(BurnInParameters()) val viewState = ViewStateAccessor( @@ -100,7 +90,7 @@ object SharedNotificationContainerBinder { * For animation sensitive coroutines, immediately run just like applicationScope does * instead of doing a post() to the main thread. This extra delay can cause visible jitter. */ - val disposableHandleMainImmediate = + disposables += view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { @@ -167,7 +157,8 @@ object SharedNotificationContainerBinder { } } - controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() }) + controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() } + disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) } view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() @@ -176,16 +167,16 @@ object SharedNotificationContainerBinder { } insets } - view.addOnLayoutChangeListener(layoutChangeListener) + disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) } - return object : DisposableHandle { - override fun dispose() { - disposableHandle.dispose() - disposableHandleMainImmediate.dispose() - controller.setOnHeightChangedRunnable(null) - view.setOnApplyWindowInsetsListener(null) - view.removeOnLayoutChangeListener(layoutChangeListener) + // Required to capture keyguard media changes and ensure the notification count is correct + val layoutChangeListener = + View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + viewModel.notificationStackChanged() } - } + view.addOnLayoutChangeListener(layoutChangeListener) + disposables += DisposableHandle { view.removeOnLayoutChangeListener(layoutChangeListener) } + + return disposables } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index ed44f20868b8..bd83121d9a34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -28,7 +28,6 @@ import com.android.systemui.statusbar.notification.stack.shared.model.StackBound import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow /** * ViewModel used by the Notification placeholders inside the scene container to update the @@ -73,7 +72,7 @@ constructor( } /** Corner rounding of the stack */ - val stackRounding: StateFlow<StackRounding> = interactor.stackRounding + val stackRounding: Flow<StackRounding> = interactor.stackRounding /** * The height in px of the contents of notification stack. Depending on the number of diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt new file mode 100644 index 000000000000..de036eaebaa2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import kotlinx.coroutines.DisposableHandle + +/** A mutable collection of [DisposableHandle] objects that is itself a [DisposableHandle] */ +class DisposableHandles : DisposableHandle { + private val handles = mutableListOf<DisposableHandle>() + + /** Add the provided handles to this collection. */ + fun add(vararg handles: DisposableHandle) { + this.handles.addAll(handles) + } + + /** Same as [add] */ + operator fun plusAssign(handle: DisposableHandle) { + this.handles.add(handle) + } + + /** Same as [add] */ + operator fun plusAssign(handles: Iterable<DisposableHandle>) { + this.handles.addAll(handles) + } + + /** [dispose] the current contents, then [add] the provided [handles] */ + fun replaceAll(vararg handles: DisposableHandle) { + dispose() + add(*handles) + } + + /** Dispose of all added handles and empty this collection. */ + override fun dispose() { + handles.forEach { it.dispose() } + handles.clear() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt index 5dd37ae46ee8..66aa572dbc48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt @@ -131,7 +131,6 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { whenever(clock.smallClock).thenReturn(smallClock) whenever(largeClock.layout).thenReturn(largeClockFaceLayout) whenever(smallClock.layout).thenReturn(smallClockFaceLayout) - whenever(clockViewModel.clock).thenReturn(clock) currentClock.value = clock } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt index 2e8160baa257..1cfca68cd452 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt @@ -222,4 +222,9 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { ) verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>()) } + + @Test + fun startButton_isDisabled_beforeIssueTypeIsSelected() { + assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled).isFalse() + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt index 546a1e019c6b..5605d1000f4e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt @@ -18,10 +18,12 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.data.repository.notificationStackAppearanceRepository val Kosmos.notificationStackAppearanceInteractor by Fixture { NotificationStackAppearanceInteractor( repository = notificationStackAppearanceRepository, + shadeInteractor = shadeInteractor, ) } diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 66abb4238726..b8ef03f36c23 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -19,6 +19,7 @@ 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; @@ -27,6 +28,7 @@ import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.PendingIntent; +import android.content.AttributionSource; import android.content.ComponentName; import android.content.IIntentSender; import android.content.Intent; @@ -39,6 +41,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.permission.IPermissionManager; +import android.permission.PermissionManager; import android.util.Slog; import com.android.internal.util.ArrayUtils; @@ -54,6 +57,9 @@ import java.util.Objects; public class BroadcastSkipPolicy { private final ActivityManagerService mService; + @Nullable + private PermissionManager mPermissionManager; + public BroadcastSkipPolicy(@NonNull ActivityManagerService service) { mService = Objects.requireNonNull(service); } @@ -283,14 +289,35 @@ public class BroadcastSkipPolicy { if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID && r.requiredPermissions != null && r.requiredPermissions.length > 0) { + final AttributionSource attributionSource; + if (usePermissionManagerForBroadcastDeliveryCheck()) { + attributionSource = + new AttributionSource.Builder(info.activityInfo.applicationInfo.uid) + .setPackageName(info.activityInfo.packageName) + .build(); + } else { + attributionSource = null; + } for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; try { - perm = AppGlobals.getPackageManager(). - checkPermission(requiredPermission, - info.activityInfo.applicationInfo.packageName, - UserHandle - .getUserId(info.activityInfo.applicationInfo.uid)); + if (usePermissionManagerForBroadcastDeliveryCheck()) { + final PermissionManager permissionManager = getPermissionManager(); + if (permissionManager != null) { + perm = permissionManager.checkPermissionForDataDelivery( + requiredPermission, attributionSource, null /* message */); + } else { + // Assume permission denial if PermissionManager is not yet available. + perm = 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; } @@ -302,11 +329,13 @@ public class BroadcastSkipPolicy { + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"; } - 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 (!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; + } } } } @@ -694,4 +723,11 @@ public class BroadcastSkipPolicy { return false; } + + private PermissionManager getPermissionManager() { + if (mPermissionManager == null) { + mPermissionManager = mService.mContext.getSystemService(PermissionManager.class); + } + return mPermissionManager; + } } diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 0209944f9fd0..fd847f11157f 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -86,3 +86,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "backstage_power" + name: "use_permission_manager_for_broadcast_delivery_check" + description: "Use PermissionManager API for broadcast delivery permission checks." + bug: "315468967" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index e8c05c6d9899..de000bf64c38 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -73,6 +73,7 @@ import android.app.role.RoleManager; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; +import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -12207,7 +12208,9 @@ public class AudioService extends IAudioService.Stub //========================================================================================== public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, IMediaProjection projection) { + boolean isVolumeController, IMediaProjection projection, + AttributionSource attributionSource) { + Objects.requireNonNull(attributionSource); AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback); if (!isPolicyRegisterAllowed(policyConfig, @@ -12228,7 +12231,8 @@ public class AudioService extends IAudioService.Stub } try { AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, pcb, hasFocusListener, - isFocusPolicy, isTestFocusPolicy, isVolumeController, projection); + isFocusPolicy, isTestFocusPolicy, isVolumeController, projection, + attributionSource); pcb.asBinder().linkToDeath(app, 0/*flags*/); // logging after registration so we have the registration id @@ -13200,6 +13204,7 @@ public class AudioService extends IAudioService.Stub public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient { private static final String TAG = "AudioPolicyProxy"; final IAudioPolicyCallback mPolicyCallback; + final AttributionSource mAttributionSource; final boolean mHasFocusListener; final boolean mIsVolumeController; final HashMap<Integer, AudioDeviceArray> mUidDeviceAffinities = @@ -13239,10 +13244,12 @@ public class AudioService extends IAudioService.Stub AudioPolicyProxy(AudioPolicyConfig config, IAudioPolicyCallback token, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, IMediaProjection projection) { + boolean isVolumeController, IMediaProjection projection, + AttributionSource attributionSource) { super(config); setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++)); mPolicyCallback = token; + mAttributionSource = attributionSource; mHasFocusListener = hasFocusListener; mIsVolumeController = isVolumeController; mProjection = projection; @@ -13370,6 +13377,7 @@ public class AudioService extends IAudioService.Stub if (android.media.audiopolicy.Flags.audioMixOwnership()) { for (AudioMix mix : mixes) { setMixRegistration(mix); + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); } int result = mAudioSystem.registerPolicyMixes(mixes, true); @@ -13393,6 +13401,9 @@ public class AudioService extends IAudioService.Stub @AudioSystem.AudioSystemError int connectMixes() { final long identity = Binder.clearCallingIdentity(); try { + for (AudioMix mix : mMixes) { + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); + } return mAudioSystem.registerPolicyMixes(mMixes, true); } finally { Binder.restoreCallingIdentity(identity); @@ -13406,6 +13417,9 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(mixesToUpdate); Objects.requireNonNull(updatedMixingRules); + for (AudioMix mix : mixesToUpdate) { + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); + } if (mixesToUpdate.length != updatedMixingRules.length) { Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules " + "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index d0a83a66dfba..cfd64c47718c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1248,7 +1248,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mService.publishLocalService(); IInputMethodManager.Stub service; if (Flags.useZeroJankProxy()) { - service = new ZeroJankProxy(mService.mHandler::post, mService); + service = + new ZeroJankProxy( + mService.mHandler::post, + mService, + () -> { + synchronized (ImfLock.class) { + return mService.isInputShown(); + } + }); } else { service = mService; } diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 396192e085e7..136ab42cd0e8 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -46,7 +46,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.util.ExceptionUtils; import android.util.Slog; import android.view.WindowManager; import android.view.inputmethod.CursorAnchorInfo; @@ -77,6 +76,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; /** * A proxy that processes all {@link IInputMethodManager} calls asynchronously. @@ -86,10 +86,12 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { private final IInputMethodManager mInner; private final Executor mExecutor; + private final BooleanSupplier mIsInputShown; - ZeroJankProxy(Executor executor, IInputMethodManager inner) { + ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) { mInner = inner; mExecutor = executor; + mIsInputShown = isInputShown; } private void offload(ThrowingRunnable r) { @@ -163,8 +165,19 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { int lastClickTooType, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) throws RemoteException { - offload(() -> mInner.showSoftInput(client, windowToken, statsToken, flags, lastClickTooType, - resultReceiver, reason)); + offload( + () -> { + if (!mInner.showSoftInput( + client, + windowToken, + statsToken, + flags, + lastClickTooType, + resultReceiver, + reason)) { + sendResultReceiverFailure(resultReceiver); + } + }); return true; } @@ -173,11 +186,24 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) throws RemoteException { - offload(() -> mInner.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver, - reason)); + offload( + () -> { + if (!mInner.hideSoftInput( + client, windowToken, statsToken, flags, resultReceiver, reason)) { + sendResultReceiverFailure(resultReceiver); + } + }); return true; } + private void sendResultReceiverFailure(ResultReceiver resultReceiver) { + resultReceiver.send( + mIsInputShown.getAsBoolean() + ? InputMethodManager.RESULT_UNCHANGED_SHOWN + : InputMethodManager.RESULT_UNCHANGED_HIDDEN, + null); + } + @Override @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) public void hideSoftInputFromServerForTest() throws RemoteException { diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index a9a82725223d..5b3934ea9b13 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -687,27 +687,20 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private static String toVolumeControlTypeString( @VolumeProvider.ControlType int volumeControlType) { - switch (volumeControlType) { - case VOLUME_CONTROL_FIXED: - return "FIXED"; - case VOLUME_CONTROL_RELATIVE: - return "RELATIVE"; - case VOLUME_CONTROL_ABSOLUTE: - return "ABSOLUTE"; - default: - return TextUtils.formatSimple("unknown(%d)", volumeControlType); - } + return switch (volumeControlType) { + case VOLUME_CONTROL_FIXED -> "FIXED"; + case VOLUME_CONTROL_RELATIVE -> "RELATIVE"; + case VOLUME_CONTROL_ABSOLUTE -> "ABSOLUTE"; + default -> TextUtils.formatSimple("unknown(%d)", volumeControlType); + }; } private static String toVolumeTypeString(@PlaybackInfo.PlaybackType int volumeType) { - switch (volumeType) { - case PLAYBACK_TYPE_LOCAL: - return "LOCAL"; - case PLAYBACK_TYPE_REMOTE: - return "REMOTE"; - default: - return TextUtils.formatSimple("unknown(%d)", volumeType); - } + return switch (volumeType) { + case PLAYBACK_TYPE_LOCAL -> "LOCAL"; + case PLAYBACK_TYPE_REMOTE -> "REMOTE"; + default -> TextUtils.formatSimple("unknown(%d)", volumeType); + }; } @Override diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 4f3cdbc52259..50ca984dcf57 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -310,6 +310,7 @@ public class PreferencesHelper implements RankingConfig { parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY), parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE), bubblePref); + r.bubblePreference = bubblePref; r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY); r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY); r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE); @@ -676,7 +677,7 @@ public class PreferencesHelper implements RankingConfig { * @param bubblePreference whether bubbles are allowed. */ public void setBubblesAllowed(String pkg, int uid, int bubblePreference) { - boolean changed = false; + boolean changed; synchronized (mPackagePreferences) { PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid); changed = p.bubblePreference != bubblePreference; diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index 37023e14eb41..953300ac43a6 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -163,7 +163,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } @Override - public void getVersion(RemoteCallback remoteCallback) throws RemoteException { + public void getVersion(RemoteCallback remoteCallback) { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion"); Objects.requireNonNull(remoteCallback); mContext.enforceCallingOrSelfPermission( @@ -244,7 +244,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { @Override public void requestFeatureDownload(Feature feature, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, IDownloadCallback downloadCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload"); Objects.requireNonNull(feature); @@ -261,16 +261,17 @@ public class OnDeviceIntelligenceManagerService extends SystemService { ensureRemoteIntelligenceServiceInitialized(); mRemoteOnDeviceIntelligenceService.run( service -> service.requestFeatureDownload(Binder.getCallingUid(), feature, - cancellationSignal, + cancellationSignalFuture, downloadCallback)); } @Override public void requestTokenInfo(Feature feature, - Bundle request, ICancellationSignal cancellationSignal, + Bundle request, + AndroidFuture cancellationSignalFuture, ITokenInfoCallback tokenInfoCallback) throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing"); + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestTokenInfo"); Objects.requireNonNull(feature); Objects.requireNonNull(request); Objects.requireNonNull(tokenInfoCallback); @@ -285,10 +286,11 @@ public class OnDeviceIntelligenceManagerService extends SystemService { PersistableBundle.EMPTY); } ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( service -> service.requestTokenInfo(Binder.getCallingUid(), feature, request, - cancellationSignal, + cancellationSignalFuture, tokenInfoCallback)); } @@ -296,8 +298,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void processRequest(Feature feature, Bundle request, int requestType, - ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IResponseCallback responseCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest"); @@ -316,7 +318,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { mRemoteInferenceService.run( service -> service.processRequest(Binder.getCallingUid(), feature, request, requestType, - cancellationSignal, processingSignal, + cancellationSignalFuture, processingSignalFuture, responseCallback)); } @@ -324,8 +326,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void processRequestStreaming(Feature feature, Bundle request, int requestType, - ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IStreamingResponseCallback streamingCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming"); Objects.requireNonNull(feature); @@ -343,7 +345,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { mRemoteInferenceService.run( service -> service.processRequestStreaming(Binder.getCallingUid(), feature, request, requestType, - cancellationSignal, processingSignal, + cancellationSignalFuture, processingSignalFuture, streamingCallback)); } @@ -356,7 +358,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { }; } - private void ensureRemoteIntelligenceServiceInitialized() throws RemoteException { + private void ensureRemoteIntelligenceServiceInitialized() { synchronized (mLock) { if (mRemoteOnDeviceIntelligenceService == null) { String serviceName = getServiceNames()[0]; @@ -388,25 +390,15 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void updateProcessingState( Bundle processingState, IProcessingUpdateStatusCallback callback) { - try { - ensureRemoteInferenceServiceInitialized(); - mRemoteInferenceService.run( - service -> service.updateProcessingState( - processingState, callback)); - } catch (RemoteException unused) { - try { - callback.onFailure( - OnDeviceIntelligenceException.PROCESSING_UPDATE_STATUS_CONNECTION_FAILED, - "Received failure invoking the remote processing service."); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to send failure status.", ex); - } - } + ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( + service -> service.updateProcessingState( + processingState, callback)); } }; } - private void ensureRemoteInferenceServiceInitialized() throws RemoteException { + private void ensureRemoteInferenceServiceInitialized() { synchronized (mLock) { if (mRemoteInferenceService == null) { String serviceName = getServiceNames()[1]; @@ -457,34 +449,38 @@ public class OnDeviceIntelligenceManagerService extends SystemService { }; } - private static void validateServiceElevated(String serviceName, boolean checkIsolated) - throws RemoteException { - if (TextUtils.isEmpty(serviceName)) { - throw new IllegalArgumentException("Received null/empty service name : " + serviceName); - } - ComponentName serviceComponent = ComponentName.unflattenFromString( - serviceName); - ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( - serviceComponent, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0); - if (serviceInfo != null) { - if (!checkIsolated) { - checkServiceRequiresPermission(serviceInfo, - Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE); - return; + private void validateServiceElevated(String serviceName, boolean checkIsolated) { + try { + if (TextUtils.isEmpty(serviceName)) { + throw new IllegalStateException( + "Remote service is not configured to complete the request"); } + ComponentName serviceComponent = ComponentName.unflattenFromString( + serviceName); + ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( + serviceComponent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0); + if (serviceInfo != null) { + if (!checkIsolated) { + checkServiceRequiresPermission(serviceInfo, + Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE); + return; + } - checkServiceRequiresPermission(serviceInfo, - Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE); - if (!isIsolatedService(serviceInfo)) { - throw new SecurityException( - "Call required an isolated service, but the configured service: " - + serviceName + ", is not isolated"); + checkServiceRequiresPermission(serviceInfo, + Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE); + if (!isIsolatedService(serviceInfo)) { + throw new SecurityException( + "Call required an isolated service, but the configured service: " + + serviceName + ", is not isolated"); + } + } else { + throw new IllegalStateException( + "Remote service is not configured to complete the request."); } - } else { - throw new RuntimeException( - "Could not find service info for serviceName: " + serviceName); + } catch (RemoteException e) { + throw new IllegalStateException("Could not fetch service info for remote services", e); } } @@ -542,7 +538,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); synchronized (mLock) { mTemporaryServiceNames = componentNames; - + mRemoteOnDeviceIntelligenceService = null; + mRemoteInferenceService = null; if (mTemporaryHandler == null) { mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { @Override diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index bfc47fdef5cb..cee6cdb06bf5 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -3962,6 +3962,20 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void testReadXml_existingPackage_bubblePrefsRestored() throws Exception { + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL); + assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O)); + + mXmlHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE); + assertEquals(BUBBLE_PREFERENCE_NONE, mXmlHelper.getBubblePreference(PKG_O, UID_O)); + + ByteArrayOutputStream stream = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL); + loadStreamXml(stream, true, UserHandle.USER_ALL); + + assertEquals(BUBBLE_PREFERENCE_ALL, mXmlHelper.getBubblePreference(PKG_O, UID_O)); + } + + @Test public void testUpdateNotificationChannel_fixedPermission() { List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0)); when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true); diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp new file mode 100644 index 000000000000..be6bea6b7fea --- /dev/null +++ b/tools/app_metadata_bundles/Android.bp @@ -0,0 +1,26 @@ +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"], +} + +java_library_host { + name: "asllib", + srcs: [ + "src/lib/java/**/*.java", + ], +} + +java_binary_host { + name: "aslgen", + manifest: "src/aslgen/aslgen.mf", + srcs: [ + "src/aslgen/java/**/*.java", + ], + static_libs: [ + "asllib", + ], +} diff --git a/tools/app_metadata_bundles/OWNERS b/tools/app_metadata_bundles/OWNERS new file mode 100644 index 000000000000..a2a250b2d5b7 --- /dev/null +++ b/tools/app_metadata_bundles/OWNERS @@ -0,0 +1,2 @@ +wenhaowang@google.com +mloh@google.com diff --git a/tools/app_metadata_bundles/README.md b/tools/app_metadata_bundles/README.md new file mode 100644 index 000000000000..6e8d287b41dd --- /dev/null +++ b/tools/app_metadata_bundles/README.md @@ -0,0 +1,9 @@ +# App metadata bundles + +This project delivers a comprehensive toolchain solution for developers +to efficiently manage app metadata bundles. + +The project consists of two subprojects: + + * A pure Java library, and + * A pure Java command-line tool. diff --git a/tools/app_metadata_bundles/src/aslgen/aslgen.mf b/tools/app_metadata_bundles/src/aslgen/aslgen.mf new file mode 100644 index 000000000000..fc656e2155a7 --- /dev/null +++ b/tools/app_metadata_bundles/src/aslgen/aslgen.mf @@ -0,0 +1 @@ +Main-Class: com.android.aslgen.Main
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java new file mode 100644 index 000000000000..df003b6aeab2 --- /dev/null +++ b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java @@ -0,0 +1,110 @@ +/* + * 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.aslgen; + +import com.android.asllib.AndroidSafetyLabel; +import com.android.asllib.AndroidSafetyLabel.Format; + +import org.xml.sax.SAXException; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +public class Main { + + /** Takes the options to make file conversion. */ + public static void main(String[] args) + throws IOException, ParserConfigurationException, SAXException, TransformerException { + + String inFile = null; + String outFile = null; + Format inFormat = Format.NULL; + Format outFormat = Format.NULL; + + + // Except for "--help", all arguments require a value currently. + // So just make sure we have an even number and + // then process them all two at a time. + if (args.length == 1 && "--help".equals(args[0])) { + showUsage(); + return; + } + if (args.length % 2 != 0) { + throw new IllegalArgumentException("Argument is missing corresponding value"); + } + for (int i = 0; i < args.length - 1; i += 2) { + final String arg = args[i].trim(); + final String argValue = args[i + 1].trim(); + if ("--in-path".equals(arg)) { + inFile = argValue; + } else if ("--out-path".equals(arg)) { + outFile = argValue; + } else if ("--in-format".equals(arg)) { + inFormat = getFormat(argValue); + } else if ("--out-format".equals(arg)) { + outFormat = getFormat(argValue); + } else { + throw new IllegalArgumentException("Unknown argument: " + arg); + } + } + + if (inFile == null) { + throw new IllegalArgumentException("input file is required"); + } + + if (outFile == null) { + throw new IllegalArgumentException("output file is required"); + } + + if (inFormat == Format.NULL) { + throw new IllegalArgumentException("input format is required"); + } + + if (outFormat == Format.NULL) { + throw new IllegalArgumentException("output format is required"); + } + + System.out.println("in path: " + inFile); + System.out.println("out path: " + outFile); + System.out.println("in format: " + inFormat); + System.out.println("out format: " + outFormat); + + var asl = AndroidSafetyLabel.readFromStream(new FileInputStream(inFile), inFormat); + asl.writeToStream(new FileOutputStream(outFile), outFormat); + } + + private static Format getFormat(String argValue) { + if ("hr".equals(argValue)) { + return Format.HUMAN_READABLE; + } else if ("od".equals(argValue)) { + return Format.ON_DEVICE; + } else { + return Format.NULL; + } + } + + private static void showUsage() { + AndroidSafetyLabel.test(); + System.err.println( + "Usage:\n" + ); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java new file mode 100644 index 000000000000..07e0e7319f4d --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java @@ -0,0 +1,102 @@ +/* + * 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +public class AndroidSafetyLabel { + + public enum Format { + NULL, HUMAN_READABLE, ON_DEVICE; + } + + private final SafetyLabels mSafetyLabels; + + public SafetyLabels getSafetyLabels() { + return mSafetyLabels; + } + + private AndroidSafetyLabel(SafetyLabels safetyLabels) { + this.mSafetyLabels = safetyLabels; + } + + /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */ + // TODO(b/329902686): Support conversion in both directions, specified by format. + public static AndroidSafetyLabel readFromStream(InputStream in, Format format) + throws IOException, ParserConfigurationException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document document = factory.newDocumentBuilder().parse(in); + + Element appMetadataBundles = + XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES); + + return AndroidSafetyLabel.createFromHrElement(appMetadataBundles); + } + + /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */ + // TODO(b/329902686): Support conversion in both directions, specified by format. + public void writeToStream(OutputStream out, Format format) + throws IOException, ParserConfigurationException, TransformerException { + var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + var document = docBuilder.newDocument(); + document.appendChild(this.toOdDomElement(document)); + + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + StreamResult streamResult = new StreamResult(out); // out + DOMSource domSource = new DOMSource(document); + transformer.transform(domSource, streamResult); + } + + /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */ + public static AndroidSafetyLabel createFromHrElement(Element appMetadataBundlesEle) { + Element safetyLabelsEle = + XmlUtils.getSingleElement(appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS); + SafetyLabels safetyLabels = SafetyLabels.createFromHrElement(safetyLabelsEle); + return new AndroidSafetyLabel(safetyLabels); + } + + /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */ + public Element toOdDomElement(Document doc) { + Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE); + aslEle.appendChild(mSafetyLabels.toOdDomElement(doc)); + return aslEle; + } + + public static void test() { + // TODO(b/329902686): Add tests. + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java new file mode 100644 index 000000000000..efdaa4062bdb --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import java.util.Map; + +/** + * Data usage category representation containing one or more {@link DataType}. Valid category keys + * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link + * DataType}, which are mapped in {@link DataTypeConstants} + */ +public class DataCategory { + private final Map<String, DataType> mDataTypes; + + private DataCategory(Map<String, DataType> dataTypes) { + this.mDataTypes = dataTypes; + } + + /** Return the type {@link Map} of String type key to {@link DataType} */ + + public Map<String, DataType> getDataTypes() { + return mDataTypes; + } + + /** Creates a {@link DataCategory} given map of {@param dataTypes}. */ + public static DataCategory create(Map<String, DataType> dataTypes) { + return new DataCategory(dataTypes); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java new file mode 100644 index 000000000000..b364c8b37194 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java @@ -0,0 +1,74 @@ +/* + * 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.asllib; + + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels}, + * {@link DataCategory}, and {@link DataType} + */ +public class DataCategoryConstants { + + public static final String CATEGORY_PERSONAL = "personal"; + public static final String CATEGORY_FINANCIAL = "financial"; + public static final String CATEGORY_LOCATION = "location"; + public static final String CATEGORY_EMAIL_TEXT_MESSAGE = "email_text_message"; + public static final String CATEGORY_PHOTO_VIDEO = "photo_video"; + public static final String CATEGORY_AUDIO = "audio"; + public static final String CATEGORY_STORAGE = "storage"; + public static final String CATEGORY_HEALTH_FITNESS = "health_fitness"; + public static final String CATEGORY_CONTACTS = "contacts"; + public static final String CATEGORY_CALENDAR = "calendar"; + public static final String CATEGORY_IDENTIFIERS = "identifiers"; + public static final String CATEGORY_APP_PERFORMANCE = "app_performance"; + public static final String CATEGORY_ACTIONS_IN_APP = "actions_in_app"; + public static final String CATEGORY_SEARCH_AND_BROWSING = "search_and_browsing"; + + /** Set of valid categories */ + public static final Set<String> VALID_CATEGORIES = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + CATEGORY_PERSONAL, + CATEGORY_FINANCIAL, + CATEGORY_LOCATION, + CATEGORY_EMAIL_TEXT_MESSAGE, + CATEGORY_PHOTO_VIDEO, + CATEGORY_AUDIO, + CATEGORY_STORAGE, + CATEGORY_HEALTH_FITNESS, + CATEGORY_CONTACTS, + CATEGORY_CALENDAR, + CATEGORY_IDENTIFIERS, + CATEGORY_APP_PERFORMANCE, + CATEGORY_ACTIONS_IN_APP, + CATEGORY_SEARCH_AND_BROWSING))); + + /** Returns {@link Set} of valid {@link String} category keys */ + public static Set<String> getValidDataCategories() { + return VALID_CATEGORIES; + } + + private DataCategoryConstants() { + /* do nothing - hide constructor */ + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java new file mode 100644 index 000000000000..d2c3d75b1d9c --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java @@ -0,0 +1,176 @@ +/* + * 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.util.HashMap; +import java.util.Map; + +/** + * Data label representation with data shared and data collected maps containing zero or more {@link + * DataCategory} + */ +public class DataLabels { + private final Map<String, DataCategory> mDataAccessed; + private final Map<String, DataCategory> mDataCollected; + private final Map<String, DataCategory> mDataShared; + + public DataLabels( + Map<String, DataCategory> dataAccessed, + Map<String, DataCategory> dataCollected, + Map<String, DataCategory> dataShared) { + mDataAccessed = dataAccessed; + mDataCollected = dataCollected; + mDataShared = dataShared; + } + + /** + * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataAccessed() { + return mDataAccessed; + } + + /** + * Returns the data collected {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataCollected() { + return mDataCollected; + } + + /** + * Returns the data shared {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataShared() { + return mDataShared; + } + + /** Creates a {@link DataLabels} from the human-readable DOM element. */ + public static DataLabels createFromHrElement(Element ele) { + Map<String, DataCategory> dataAccessed = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED); + Map<String, DataCategory> dataCollected = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED); + Map<String, DataCategory> dataShared = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED); + return new DataLabels(dataAccessed, dataCollected, dataShared); + } + + private static Map<String, DataCategory> getDataCategoriesWithTag( + Element dataLabelsEle, String dataCategoryUsageTypeTag) { + Map<String, Map<String, DataType>> dataTypeMap = + new HashMap<String, Map<String, DataType>>(); + NodeList dataSharedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag); + + for (int i = 0; i < dataSharedNodeList.getLength(); i++) { + Element dataSharedEle = (Element) dataSharedNodeList.item(i); + String dataCategoryName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY); + String dataTypeName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE); + + if (!dataTypeMap.containsKey((dataCategoryName))) { + dataTypeMap.put(dataCategoryName, new HashMap<String, DataType>()); + } + dataTypeMap + .get(dataCategoryName) + .put(dataTypeName, DataType.createFromHrElement(dataSharedEle)); + } + + Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>(); + for (String dataCategoryName : dataTypeMap.keySet()) { + Map<String, DataType> dataTypes = dataTypeMap.get(dataCategoryName); + dataCategoryMap.put(dataCategoryName, DataCategory.create(dataTypes)); + } + return dataCategoryMap; + } + + /** Gets the on-device DOM element for the {@link DataLabels}. */ + public Element toOdDomElement(Document doc) { + Element dataLabelsEle = + XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS); + + maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED); + maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED); + maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED); + + return dataLabelsEle; + } + + private void maybeAppendDataUsages( + Document doc, + Element dataLabelsEle, + Map<String, DataCategory> dataCategoriesMap, + String dataUsageTypeName) { + if (dataCategoriesMap.isEmpty()) { + return; + } + Element dataUsageEle = XmlUtils.createPbundleEleWithName(doc, dataUsageTypeName); + + for (String dataCategoryName : dataCategoriesMap.keySet()) { + Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, dataCategoryName); + DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName); + for (String dataTypeName : dataCategory.getDataTypes().keySet()) { + DataType dataType = dataCategory.getDataTypes().get(dataTypeName); + Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, dataTypeName); + if (!dataType.getPurposeSet().isEmpty()) { + Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY); + purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES); + purposesEle.setAttribute( + XmlUtils.OD_ATTR_NUM, String.valueOf(dataType.getPurposeSet().size())); + for (DataType.Purpose purpose : dataType.getPurposeSet()) { + Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM); + purposeEle.setAttribute( + XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue())); + purposesEle.appendChild(purposeEle); + } + dataTypeEle.appendChild(purposesEle); + } + + maybeAddBoolToOdElement( + doc, + dataTypeEle, + dataType.getIsCollectionOptional(), + XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL); + maybeAddBoolToOdElement( + doc, + dataTypeEle, + dataType.getIsSharingOptional(), + XmlUtils.OD_NAME_IS_SHARING_OPTIONAL); + maybeAddBoolToOdElement( + doc, dataTypeEle, dataType.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL); + + dataCategoryEle.appendChild(dataTypeEle); + } + dataUsageEle.appendChild(dataCategoryEle); + } + dataLabelsEle.appendChild(dataUsageEle); + } + + private static void maybeAddBoolToOdElement( + Document doc, Element parentEle, Boolean b, String odName) { + if (b == null) { + return; + } + Element ele = XmlUtils.createOdBooleanEle(doc, odName, b); + parentEle.appendChild(ele); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java new file mode 100644 index 000000000000..7451c6923113 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java @@ -0,0 +1,145 @@ +/* + * 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.asllib; + +import org.w3c.dom.Element; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Data usage type representation. Types are specific to a {@link DataCategory} and contains + * metadata related to the data usage purpose. + */ +public class DataType { + public enum Purpose { + PURPOSE_APP_FUNCTIONALITY(1), + PURPOSE_ANALYTICS(2), + PURPOSE_DEVELOPER_COMMUNICATIONS(3), + PURPOSE_FRAUD_PREVENTION_SECURITY(4), + PURPOSE_ADVERTISING(5), + PURPOSE_PERSONALIZATION(6), + PURPOSE_ACCOUNT_MANAGEMENT(7); + + private static final String PURPOSE_PREFIX = "PURPOSE_"; + + private final int mValue; + + Purpose(int value) { + this.mValue = value; + } + + /** Get the int value associated with the Purpose. */ + public int getValue() { + return mValue; + } + + /** Get the Purpose associated with the int value. */ + public static Purpose forValue(int value) { + for (Purpose e : values()) { + if (e.getValue() == value) { + return e; + } + } + throw new IllegalArgumentException("No enum for value: " + value); + } + + /** Get the Purpose associated with the human-readable String. */ + public static Purpose forString(String s) { + for (Purpose e : values()) { + if (e.toString().equals(s)) { + return e; + } + } + throw new IllegalArgumentException("No enum for str: " + s); + } + + /** Human-readable String representation of Purpose. */ + public String toString() { + if (!this.name().startsWith(PURPOSE_PREFIX)) { + return this.name(); + } + return this.name().substring(PURPOSE_PREFIX.length()).toLowerCase(); + } + } + + private final Set<Purpose> mPurposeSet; + private final Boolean mIsCollectionOptional; + private final Boolean mIsSharingOptional; + private final Boolean mEphemeral; + + private DataType( + Set<Purpose> purposeSet, + Boolean isCollectionOptional, + Boolean isSharingOptional, + Boolean ephemeral) { + this.mPurposeSet = purposeSet; + this.mIsCollectionOptional = isCollectionOptional; + this.mIsSharingOptional = isSharingOptional; + this.mEphemeral = ephemeral; + } + + /** + * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category + * and type + */ + public Set<Purpose> getPurposeSet() { + return mPurposeSet; + } + + /** + * For data-collected, returns {@code true} if data usage is user optional and {@code false} if + * data usage is required. Should return {@code null} for data-accessed and data-shared. + */ + public Boolean getIsCollectionOptional() { + return mIsCollectionOptional; + } + + /** + * For data-shared, returns {@code true} if data usage is user optional and {@code false} if + * data usage is required. Should return {@code null} for data-accessed and data-collected. + */ + public Boolean getIsSharingOptional() { + return mIsSharingOptional; + } + + /** + * For data-collected, returns {@code true} if data usage is user optional and {@code false} if + * data usage is processed ephemerally. Should return {@code null} for data-shared. + */ + public Boolean getEphemeral() { + return mEphemeral; + } + + /** Creates a {@link DataType} from the human-readable DOM element. */ + public static DataType createFromHrElement(Element hrDataTypeEle) { + Set<Purpose> purposeSet = + Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|")) + .map(Purpose::forString) + .collect(Collectors.toUnmodifiableSet()); + Boolean isCollectionOptional = + XmlUtils.fromString( + hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL)); + Boolean isSharingOptional = + XmlUtils.fromString( + hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL)); + Boolean ephemeral = + XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL)); + return new DataType(purposeSet, isCollectionOptional, isSharingOptional, ephemeral); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java new file mode 100644 index 000000000000..a0a75377e988 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels}, + * {@link DataCategory}, and {@link DataType} + */ +public class DataTypeConstants { + /** Data types for {@link DataCategoryConstants.CATEGORY_PERSONAL} */ + public static final String TYPE_NAME = "name"; + + public static final String TYPE_EMAIL_ADDRESS = "email_address"; + public static final String TYPE_PHONE_NUMBER = "phone_number"; + public static final String TYPE_RACE_ETHNICITY = "race_ethnicity"; + public static final String TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS = + "political_or_religious_beliefs"; + public static final String TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY = + "sexual_orientation_or_gender_identity"; + public static final String TYPE_PERSONAL_IDENTIFIERS = "personal_identifiers"; + public static final String TYPE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_FINANCIAL} */ + public static final String TYPE_CARD_BANK_ACCOUNT = "card_bank_account"; + + public static final String TYPE_PURCHASE_HISTORY = "purchase_history"; + public static final String TYPE_CREDIT_SCORE = "credit_score"; + public static final String TYPE_FINANCIAL_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_LOCATION} */ + public static final String TYPE_APPROX_LOCATION = "approx_location"; + + public static final String TYPE_PRECISE_LOCATION = "precise_location"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE} */ + public static final String TYPE_EMAILS = "emails"; + + public static final String TYPE_TEXT_MESSAGES = "text_messages"; + public static final String TYPE_EMAIL_TEXT_MESSAGE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_PHOTO_VIDEO} */ + public static final String TYPE_PHOTOS = "photos"; + + public static final String TYPE_VIDEOS = "videos"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_AUDIO} */ + public static final String TYPE_SOUND_RECORDINGS = "sound_recordings"; + + public static final String TYPE_MUSIC_FILES = "music_files"; + public static final String TYPE_AUDIO_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_STORAGE} */ + public static final String TYPE_FILES_DOCS = "files_docs"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_HEALTH_FITNESS} */ + public static final String TYPE_HEALTH = "health"; + + public static final String TYPE_FITNESS = "fitness"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_CONTACTS} */ + public static final String TYPE_CONTACTS = "contacts"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_CALENDAR} */ + public static final String TYPE_CALENDAR = "calendar"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_IDENTIFIERS} */ + public static final String TYPE_IDENTIFIERS_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_APP_PERFORMANCE} */ + public static final String TYPE_CRASH_LOGS = "crash_logs"; + + public static final String TYPE_PERFORMANCE_DIAGNOSTICS = "performance_diagnostics"; + public static final String TYPE_APP_PERFORMANCE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_ACTIONS_IN_APP} */ + public static final String TYPE_USER_INTERACTION = "user_interaction"; + + public static final String TYPE_IN_APP_SEARCH_HISTORY = "in_app_search_history"; + public static final String TYPE_INSTALLED_APPS = "installed_apps"; + public static final String TYPE_USER_GENERATED_CONTENT = "user_generated_content"; + public static final String TYPE_ACTIONS_IN_APP_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING} */ + public static final String TYPE_WEB_BROWSING_HISTORY = "web_browsing_history"; + + /** Set of valid categories */ + public static final Set<String> VALID_TYPES = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + TYPE_NAME, + TYPE_EMAIL_ADDRESS, + TYPE_PHONE_NUMBER, + TYPE_RACE_ETHNICITY, + TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS, + TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY, + TYPE_PERSONAL_IDENTIFIERS, + TYPE_OTHER, + TYPE_CARD_BANK_ACCOUNT, + TYPE_PURCHASE_HISTORY, + TYPE_CREDIT_SCORE, + TYPE_FINANCIAL_OTHER, + TYPE_APPROX_LOCATION, + TYPE_PRECISE_LOCATION, + TYPE_EMAILS, + TYPE_TEXT_MESSAGES, + TYPE_EMAIL_TEXT_MESSAGE_OTHER, + TYPE_PHOTOS, + TYPE_VIDEOS, + TYPE_SOUND_RECORDINGS, + TYPE_MUSIC_FILES, + TYPE_AUDIO_OTHER, + TYPE_FILES_DOCS, + TYPE_HEALTH, + TYPE_FITNESS, + TYPE_CONTACTS, + TYPE_CALENDAR, + TYPE_IDENTIFIERS_OTHER, + TYPE_CRASH_LOGS, + TYPE_PERFORMANCE_DIAGNOSTICS, + TYPE_APP_PERFORMANCE_OTHER, + TYPE_USER_INTERACTION, + TYPE_IN_APP_SEARCH_HISTORY, + TYPE_INSTALLED_APPS, + TYPE_USER_GENERATED_CONTENT, + TYPE_ACTIONS_IN_APP_OTHER, + TYPE_WEB_BROWSING_HISTORY))); + + /** Returns {@link Set} of valid {@link String} category keys */ + public static Set<String> getValidDataTypes() { + return VALID_TYPES; + } + + private DataTypeConstants() { + /* do nothing - hide constructor */ + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java new file mode 100644 index 000000000000..6ba15e1ec4db --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java @@ -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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** Safety Label representation containing zero or more {@link DataCategory} for data shared */ +public class SafetyLabels { + + private final Long mVersion; + private final DataLabels mDataLabels; + + private SafetyLabels(Long version, DataLabels dataLabels) { + this.mVersion = version; + this.mDataLabels = dataLabels; + } + + /** Returns the data label for the safety label */ + public DataLabels getDataLabel() { + return mDataLabels; + } + + /** Gets the version of the {@link SafetyLabels}. */ + public Long getVersion() { + return mVersion; + } + + /** Creates a {@link SafetyLabels} from the human-readable DOM element. */ + public static SafetyLabels createFromHrElement(Element safetyLabelsEle) { + Long version; + try { + version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION)); + } catch (Exception e) { + throw new IllegalArgumentException( + "Malformed or missing required version in safety labels."); + } + Element dataLabelsEle = + XmlUtils.getSingleElement(safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS); + DataLabels dataLabels = DataLabels.createFromHrElement(dataLabelsEle); + return new SafetyLabels(version, dataLabels); + } + + /** Creates an on-device DOM element from the {@link SafetyLabels}. */ + public Element toOdDomElement(Document doc) { + Element safetyLabelsEle = + XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS); + safetyLabelsEle.appendChild(mDataLabels.toOdDomElement(doc)); + return safetyLabelsEle; + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java new file mode 100644 index 000000000000..4392c2c220c4 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java @@ -0,0 +1,119 @@ +/* + * 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +public class XmlUtils { + public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles"; + public static final String HR_TAG_SAFETY_LABELS = "safety-labels"; + public static final String HR_TAG_DATA_LABELS = "data-labels"; + public static final String HR_TAG_DATA_ACCESSED = "data-accessed"; + public static final String HR_TAG_DATA_COLLECTED = "data-collected"; + public static final String HR_TAG_DATA_SHARED = "data-shared"; + + public static final String HR_ATTR_DATA_CATEGORY = "dataCategory"; + public static final String HR_ATTR_DATA_TYPE = "dataType"; + public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional"; + public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional"; + public static final String HR_ATTR_EPHEMERAL = "ephemeral"; + public static final String HR_ATTR_PURPOSES = "purposes"; + public static final String HR_ATTR_VERSION = "version"; + + public static final String OD_TAG_BUNDLE = "bundle"; + public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map"; + public static final String OD_TAG_BOOLEAN = "boolean"; + public static final String OD_TAG_INT_ARRAY = "int-array"; + public static final String OD_TAG_ITEM = "item"; + public static final String OD_ATTR_NAME = "name"; + public static final String OD_ATTR_VALUE = "value"; + public static final String OD_ATTR_NUM = "num"; + public static final String OD_NAME_SAFETY_LABELS = "safety_labels"; + public static final String OD_NAME_DATA_LABELS = "data_labels"; + public static final String OD_NAME_DATA_ACCESSED = "data_accessed"; + public static final String OD_NAME_DATA_COLLECTED = "data_collected"; + public static final String OD_NAME_DATA_SHARED = "data_shared"; + public static final String OD_NAME_PURPOSES = "purposes"; + public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional"; + public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional"; + public static final String OD_NAME_EPHEMERAL = "ephemeral"; + + public static final String TRUE_STR = "true"; + public static final String FALSE_STR = "false"; + + /** Gets the single top-level {@link Element} having the {@param tagName}. */ + public static Element getSingleElement(Document doc, String tagName) { + var elements = doc.getElementsByTagName(tagName); + return getSingleElement(elements, tagName); + } + + /** + * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}. + */ + public static Element getSingleElement(Element parentEle, String tagName) { + var elements = parentEle.getElementsByTagName(tagName); + return getSingleElement(elements, tagName); + } + + /** Gets the single {@link Element} from {@param elements} and having the {@param tagName}. */ + public static Element getSingleElement(NodeList elements, String tagName) { + if (elements.getLength() != 1) { + throw new IllegalArgumentException( + String.format("Expected 1 %s but got %s.", tagName, elements.getLength())); + } + var elementAsNode = elements.item(0); + if (!(elementAsNode instanceof Element)) { + throw new IllegalStateException(String.format("%s was not an element.", tagName)); + } + return ((Element) elementAsNode); + } + + /** Gets the Boolean from the String value. */ + public static Boolean fromString(String s) { + if (s == null) { + return null; + } + if (s.equals(TRUE_STR)) { + return true; + } else if (s.equals(FALSE_STR)) { + return false; + } + return null; + } + + /** Creates an on-device PBundle DOM Element with the given attribute name. */ + public static Element createPbundleEleWithName(Document doc, String name) { + var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP); + ele.setAttribute(XmlUtils.OD_ATTR_NAME, name); + return ele; + } + + /** Create an on-device Boolean DOM Element with the given attribute name. */ + public static Element createOdBooleanEle(Document doc, String name, boolean b) { + var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN); + ele.setAttribute(XmlUtils.OD_ATTR_NAME, name); + ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b)); + return ele; + } + + /** Returns whether the String is null or empty. */ + public static boolean isNullOrEmpty(String s) { + return s == null || s.isEmpty(); + } +} |