diff options
84 files changed, 2721 insertions, 1352 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 4d3e252a90e3..a551f0d794e2 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2190,14 +2190,6 @@ package android.app.job { package android.app.ondeviceintelligence { - @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class Content implements android.os.Parcelable { - ctor public Content(@NonNull android.os.Bundle); - method public int describeContents(); - method @NonNull public android.os.Bundle getData(); - method public void writeToParcel(@NonNull android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.app.ondeviceintelligence.Content> CREATOR; - } - @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface DownloadCallback { method public void onDownloadCompleted(@NonNull android.os.PersistableBundle); method public void onDownloadFailed(int, @Nullable String, @NonNull android.os.PersistableBundle); @@ -2233,11 +2225,11 @@ package android.app.ondeviceintelligence { } @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class FeatureDetails implements android.os.Parcelable { - ctor public FeatureDetails(@android.app.ondeviceintelligence.FeatureDetails.Status int, @NonNull android.os.PersistableBundle); - ctor public FeatureDetails(@android.app.ondeviceintelligence.FeatureDetails.Status int); + ctor public FeatureDetails(int, @NonNull android.os.PersistableBundle); + ctor public FeatureDetails(int); method public int describeContents(); method @NonNull public android.os.PersistableBundle getFeatureDetailParams(); - method @android.app.ondeviceintelligence.FeatureDetails.Status public int getFeatureStatus(); + method public int getFeatureStatus(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.ondeviceintelligence.FeatureDetails> CREATOR; field public static final int FEATURE_STATUS_AVAILABLE = 3; // 0x3 @@ -2247,35 +2239,14 @@ package android.app.ondeviceintelligence { field public static final int FEATURE_STATUS_UNAVAILABLE = 0; // 0x0 } - @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD}) public static @interface FeatureDetails.Status { - } - - @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public class OnDeviceIntelligenceManager { - method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeature(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>); - method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeatureDetails(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>); - method @Nullable @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public String getRemoteServicePackageName(); - method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getVersion(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.LongConsumer); - method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void listFeatures(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>); - method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequest(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.ProcessingOutcomeReceiver); - method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequestStreaming(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver); - method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestFeatureDownload(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.DownloadCallback); - method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestTokenInfo(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.TokenInfo,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>); - field public static final int REQUEST_TYPE_EMBEDDINGS = 2; // 0x2 - field public static final int REQUEST_TYPE_INFERENCE = 0; // 0x0 - field public static final int REQUEST_TYPE_PREPARE = 1; // 0x1 - } - - public static class OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException extends java.lang.Exception { - ctor public OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException(int, @NonNull String, @NonNull android.os.PersistableBundle); - ctor public OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException(int, @NonNull android.os.PersistableBundle); + @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public class OnDeviceIntelligenceException extends java.lang.Exception { + ctor public OnDeviceIntelligenceException(int, @NonNull String, @NonNull android.os.PersistableBundle); + ctor public OnDeviceIntelligenceException(int, @NonNull android.os.PersistableBundle); + ctor public OnDeviceIntelligenceException(int, @NonNull String); + ctor public OnDeviceIntelligenceException(int); method public int getErrorCode(); method @NonNull public android.os.PersistableBundle getErrorParams(); - field public static final int ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE = 1000; // 0x3e8 - } - - public static class OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException extends android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException { - ctor public OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException(int, @NonNull String, @NonNull android.os.PersistableBundle); - ctor public OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException(int, @NonNull android.os.PersistableBundle); + field public static final int ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE = 100; // 0x64 field public static final int PROCESSING_ERROR_BAD_DATA = 2; // 0x2 field public static final int PROCESSING_ERROR_BAD_REQUEST = 3; // 0x3 field public static final int PROCESSING_ERROR_BUSY = 9; // 0x9 @@ -2291,10 +2262,28 @@ package android.app.ondeviceintelligence { field public static final int PROCESSING_ERROR_SERVICE_UNAVAILABLE = 15; // 0xf field public static final int PROCESSING_ERROR_SUSPENDED = 13; // 0xd field public static final int PROCESSING_ERROR_UNKNOWN = 1; // 0x1 + field public static final int PROCESSING_UPDATE_STATUS_CONNECTION_FAILED = 200; // 0xc8 + } + + @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class OnDeviceIntelligenceManager { + method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeature(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); + method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeatureDetails(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); + method @Nullable @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public String getRemoteServicePackageName(); + method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getVersion(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.LongConsumer); + method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void listFeatures(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); + method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequest(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.Bundle, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.ProcessingCallback); + method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequestStreaming(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.Bundle, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.StreamingProcessingCallback); + method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestFeatureDownload(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.DownloadCallback); + method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestTokenInfo(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.Bundle, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.TokenInfo,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); + field public static final int REQUEST_TYPE_EMBEDDINGS = 2; // 0x2 + field public static final int REQUEST_TYPE_INFERENCE = 0; // 0x0 + field public static final int REQUEST_TYPE_PREPARE = 1; // 0x1 } - @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface ProcessingOutcomeReceiver extends android.os.OutcomeReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> { - method public default void onDataAugmentRequest(@NonNull android.app.ondeviceintelligence.Content, @NonNull java.util.function.Consumer<android.app.ondeviceintelligence.Content>); + @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface ProcessingCallback { + method public default void onDataAugmentRequest(@NonNull android.os.Bundle, @NonNull java.util.function.Consumer<android.os.Bundle>); + method public void onError(@NonNull android.app.ondeviceintelligence.OnDeviceIntelligenceException); + method public void onResult(@NonNull android.os.Bundle); } @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class ProcessingSignal { @@ -2307,8 +2296,8 @@ package android.app.ondeviceintelligence { method public void onSignalReceived(@NonNull android.os.PersistableBundle); } - @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface StreamedProcessingOutcomeReceiver extends android.app.ondeviceintelligence.ProcessingOutcomeReceiver { - method public void onNewContent(@NonNull android.app.ondeviceintelligence.Content); + @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface StreamingProcessingCallback extends android.app.ondeviceintelligence.ProcessingCallback { + method public void onPartialResult(@NonNull android.os.Bundle); } @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class TokenInfo implements android.os.Parcelable { @@ -12956,38 +12945,26 @@ package android.service.ondeviceintelligence { ctor public OnDeviceIntelligenceService(); method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent); method public abstract void onDownloadFeature(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull android.app.ondeviceintelligence.DownloadCallback); - method public abstract void onGetFeature(int, int, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>); - method public abstract void onGetFeatureDetails(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>); + method public abstract void onGetFeature(int, int, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); + method public abstract void onGetFeatureDetails(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); method public abstract void onGetReadOnlyFeatureFileDescriptorMap(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.function.Consumer<java.util.Map<java.lang.String,android.os.ParcelFileDescriptor>>); method public abstract void onGetVersion(@NonNull java.util.function.LongConsumer); method public abstract void onInferenceServiceConnected(); method public abstract void onInferenceServiceDisconnected(); - method public abstract void onListFeatures(int, @NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>); - method public final void updateProcessingState(@NonNull android.os.Bundle, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PersistableBundle,android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceUpdateProcessingException>); + method public abstract void onListFeatures(int, @NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); + method public final void updateProcessingState(@NonNull android.os.Bundle, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PersistableBundle,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceIntelligenceService"; } - public abstract static class OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException extends java.lang.Exception { - ctor public OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException(int); - ctor public OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException(int, @NonNull String); - method public int getErrorCode(); - } - - public static class OnDeviceIntelligenceService.OnDeviceUpdateProcessingException extends android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException { - ctor public OnDeviceIntelligenceService.OnDeviceUpdateProcessingException(int); - ctor public OnDeviceIntelligenceService.OnDeviceUpdateProcessingException(int, @NonNull String); - field public static final int PROCESSING_UPDATE_STATUS_CONNECTION_FAILED = 1; // 0x1 - } - @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public abstract class OnDeviceSandboxedInferenceService extends android.app.Service { ctor public OnDeviceSandboxedInferenceService(); method public final void fetchFeatureFileInputStreamMap(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.Map<java.lang.String,java.io.FileInputStream>>); method @NonNull public java.util.concurrent.Executor getCallbackExecutor(); method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent); - method @NonNull public abstract void onProcessRequest(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.ProcessingOutcomeReceiver); - method @NonNull public abstract void onProcessRequestStreaming(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver); - method @NonNull public abstract void onTokenInfoRequest(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.TokenInfo,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>); - method public abstract void onUpdateProcessingState(@NonNull android.os.Bundle, @NonNull android.os.OutcomeReceiver<android.os.PersistableBundle,android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceUpdateProcessingException>); + method @NonNull public abstract void onProcessRequest(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.Bundle, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.ProcessingCallback); + method @NonNull public abstract void onProcessRequestStreaming(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.Bundle, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.StreamingProcessingCallback); + method @NonNull public abstract void onTokenInfoRequest(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.Bundle, @Nullable android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.TokenInfo,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); + method public abstract void onUpdateProcessingState(@NonNull android.os.Bundle, @NonNull android.os.OutcomeReceiver<android.os.PersistableBundle,android.app.ondeviceintelligence.OnDeviceIntelligenceException>); method public final java.io.FileInputStream openFileInput(@NonNull String) throws java.io.FileNotFoundException; method public final void openFileInputAsync(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.io.FileInputStream>) throws java.io.FileNotFoundException; field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService"; diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt index 1e72a061d35a..62fc67b8dedf 100644 --- a/core/api/system-lint-baseline.txt +++ b/core/api/system-lint-baseline.txt @@ -1885,8 +1885,6 @@ Todo: android.Manifest.permission#READ_PEOPLE_DATA: Documentation mentions 'TODO' Todo: android.app.NotificationManager#isNotificationAssistantAccessGranted(android.content.ComponentName): Documentation mentions 'TODO' -Todo: android.app.ondeviceintelligence.OnDeviceIntelligenceManager#requestFeatureDownload(android.app.ondeviceintelligence.Feature, android.app.ondeviceintelligence.CancellationSignal, java.util.concurrent.Executor, android.app.ondeviceintelligence.DownloadCallback): - Documentation mentions 'TODO' Todo: android.hardware.camera2.params.StreamConfigurationMap: Documentation mentions 'TODO' Todo: android.hardware.location.ContextHubManager#getNanoAppInstanceInfo(int): diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index d01626e17f2d..fa4a4009ebc9 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -34,6 +34,8 @@ import android.app.contentsuggestions.ContentSuggestionsManager; import android.app.contentsuggestions.IContentSuggestionsManager; import android.app.ecm.EnhancedConfirmationFrameworkInitializer; import android.app.job.JobSchedulerFrameworkInitializer; +import android.app.ondeviceintelligence.IOnDeviceIntelligenceManager; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager; import android.app.people.PeopleManager; import android.app.prediction.AppPredictionManager; import android.app.role.RoleFrameworkInitializer; @@ -1589,6 +1591,19 @@ public final class SystemServiceRegistry { return new WearableSensingManager(ctx.getOuterContext(), manager); }}); + registerService(Context.ON_DEVICE_INTELLIGENCE_SERVICE, OnDeviceIntelligenceManager.class, + new CachedServiceFetcher<OnDeviceIntelligenceManager>() { + @Override + public OnDeviceIntelligenceManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder iBinder = ServiceManager.getServiceOrThrow( + Context.ON_DEVICE_INTELLIGENCE_SERVICE); + IOnDeviceIntelligenceManager manager = + IOnDeviceIntelligenceManager.Stub.asInterface(iBinder); + return new OnDeviceIntelligenceManager(ctx.getOuterContext(), manager); + } + }); + registerService(Context.GRAMMATICAL_INFLECTION_SERVICE, GrammaticalInflectionManager.class, new CachedServiceFetcher<GrammaticalInflectionManager>() { @Override diff --git a/core/java/android/app/ondeviceintelligence/Content.java b/core/java/android/app/ondeviceintelligence/Content.java deleted file mode 100644 index 51bd156fc946..000000000000 --- a/core/java/android/app/ondeviceintelligence/Content.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.app.ondeviceintelligence; - -import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; - -import android.annotation.FlaggedApi; -import android.annotation.NonNull; -import android.annotation.SystemApi; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; - -import java.util.Objects; - -/** - * Represents content sent to and received from the on-device inference service. - * Can contain a collection of text, image, and binary parts or any combination of these. - * - * @hide - */ -@SystemApi -@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) -public final class Content implements Parcelable { - //TODO: Improve javadoc after adding validation logic. - private static final String TAG = "Content"; - private final Bundle mData; - - /** - * Create a content object using a Bundle of only known types that are read-only. - */ - public Content(@NonNull Bundle data) { - Objects.requireNonNull(data); - validateBundleData(data); - this.mData = data; - } - - /** - * Returns the Content's data represented as a Bundle. - */ - @NonNull - public Bundle getData() { - return mData; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeBundle(mData); - } - - @Override - public int describeContents() { - int mask = 0; - mask |= mData.describeContents(); - return mask; - } - - @NonNull - public static final Creator<Content> CREATOR = new Creator<>() { - @Override - @NonNull - public Content createFromParcel(@NonNull Parcel in) { - return new Content(in.readBundle(getClass().getClassLoader())); - } - - @Override - @NonNull - public Content[] newArray(int size) { - return new Content[size]; - } - }; - - private void validateBundleData(Bundle unused) { - // TODO: Validate there are only known types. - } -} diff --git a/core/java/android/app/ondeviceintelligence/DownloadCallback.java b/core/java/android/app/ondeviceintelligence/DownloadCallback.java index 684c71f9144c..30c6e1924942 100644 --- a/core/java/android/app/ondeviceintelligence/DownloadCallback.java +++ b/core/java/android/app/ondeviceintelligence/DownloadCallback.java @@ -105,7 +105,7 @@ public interface DownloadCallback { } /** - * Called when model download via MDD completed. The remote implementation can populate any + * Called when model download is completed. The remote implementation can populate any * associated download params like file stats etc. in this callback to inform the client. * * @param downloadParams params containing info about the completed download. diff --git a/core/java/android/app/ondeviceintelligence/Feature.java b/core/java/android/app/ondeviceintelligence/Feature.java index 4a38c9224a3d..fd0379a046cc 100644 --- a/core/java/android/app/ondeviceintelligence/Feature.java +++ b/core/java/android/app/ondeviceintelligence/Feature.java @@ -34,7 +34,6 @@ import android.os.PersistableBundle; @SystemApi @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) public final class Feature implements Parcelable { - // TODO(b/325315604) - Check if we can expose non-hidden IntDefs in Framework. private final int mId; @Nullable private final String mName; diff --git a/core/java/android/app/ondeviceintelligence/FeatureDetails.java b/core/java/android/app/ondeviceintelligence/FeatureDetails.java index f3cbd2621694..44930f2c34f4 100644 --- a/core/java/android/app/ondeviceintelligence/FeatureDetails.java +++ b/core/java/android/app/ondeviceintelligence/FeatureDetails.java @@ -60,6 +60,9 @@ public final class FeatureDetails implements Parcelable { /** Underlying service is unavailable and feature status cannot be fetched. */ public static final int FEATURE_STATUS_SERVICE_UNAVAILABLE = 4; + /** + * @hide + */ @IntDef(value = { FEATURE_STATUS_UNAVAILABLE, FEATURE_STATUS_DOWNLOADABLE, diff --git a/core/java/android/app/ondeviceintelligence/IDownloadCallback.aidl b/core/java/android/app/ondeviceintelligence/IDownloadCallback.aidl index aba563f84e1b..8fc269ea6dac 100644 --- a/core/java/android/app/ondeviceintelligence/IDownloadCallback.aidl +++ b/core/java/android/app/ondeviceintelligence/IDownloadCallback.aidl @@ -16,15 +16,14 @@ package android.app.ondeviceintelligence; -import android.app.ondeviceintelligence.IProcessingSignal; import android.os.PersistableBundle; /** - * Interface for Download callback to passed onto service implementation, + * Interface for Download callback to be passed onto service implementation, * * @hide */ -oneway interface IDownloadCallback { +interface IDownloadCallback { void onDownloadStarted(long bytesToDownload) = 1; void onDownloadProgress(long bytesDownloaded) = 2; void onDownloadFailed(int failureStatus, String errorMessage, in PersistableBundle errorParams) = 3; diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl index 360a8094723c..0dbe18156904 100644 --- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl +++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl @@ -21,7 +21,7 @@ import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.RemoteCallback; - import android.app.ondeviceintelligence.Content; + import android.os.Bundle; import android.app.ondeviceintelligence.Feature; import android.app.ondeviceintelligence.FeatureDetails; import android.app.ondeviceintelligence.IDownloadCallback; @@ -39,7 +39,7 @@ * * @hide */ - oneway interface IOnDeviceIntelligenceManager { + interface IOnDeviceIntelligenceManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") void getVersion(in RemoteCallback remoteCallback) = 1; @@ -53,18 +53,20 @@ 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, ICancellationSignal signal, in IDownloadCallback callback) = 5; + void requestFeatureDownload(in Feature feature, in ICancellationSignal signal, in IDownloadCallback callback) = 5; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void requestTokenInfo(in Feature feature, in Content request, in ICancellationSignal signal, + void requestTokenInfo(in Feature feature, in Bundle requestBundle, in ICancellationSignal signal, in ITokenInfoCallback tokenInfocallback) = 6; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void processRequest(in Feature feature, in Content request, int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal signal, - in IResponseCallback responseCallback) = 7; + void processRequest(in Feature feature, in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, + in IProcessingSignal signal, in IResponseCallback responseCallback) = 7; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") void processRequestStreaming(in Feature feature, - in Content request, int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal signal, + in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal signal, in IStreamingResponseCallback streamingCallback) = 8; + + String getRemoteServicePackageName() = 9; } diff --git a/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl b/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl index 0adf305f2920..45963d2af4e6 100644 --- a/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl +++ b/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl @@ -1,8 +1,7 @@ package android.app.ondeviceintelligence; -import android.app.ondeviceintelligence.Content; -import android.app.ondeviceintelligence.IProcessingSignal; import android.os.PersistableBundle; +import android.os.Bundle; import android.os.RemoteCallback; /** @@ -11,7 +10,7 @@ import android.os.RemoteCallback; * @hide */ interface IResponseCallback { - void onSuccess(in Content result) = 1; + void onSuccess(in Bundle resultBundle) = 1; void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2; - void onDataAugmentRequest(in Content content, in RemoteCallback contentCallback) = 3; + void onDataAugmentRequest(in Bundle processedContent, in RemoteCallback responseCallback) = 3; } diff --git a/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl b/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl index 132e53e1ae2e..671abe31ce86 100644 --- a/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl +++ b/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl @@ -1,10 +1,8 @@ package android.app.ondeviceintelligence; -import android.app.ondeviceintelligence.Content; -import android.app.ondeviceintelligence.IResponseCallback; -import android.app.ondeviceintelligence.IProcessingSignal; import android.os.PersistableBundle; import android.os.RemoteCallback; +import android.os.Bundle; /** @@ -13,8 +11,8 @@ import android.os.RemoteCallback; * @hide */ interface IStreamingResponseCallback { - void onNewContent(in Content result) = 1; - void onSuccess(in Content result) = 2; + void onNewContent(in Bundle processedResult) = 1; + void onSuccess(in Bundle result) = 2; void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 3; - void onDataAugmentRequest(in Content content, in RemoteCallback contentCallback) = 4; + void onDataAugmentRequest(in Bundle processedContent, in RemoteCallback responseCallback) = 4; } diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java new file mode 100644 index 000000000000..03ff563a88c0 --- /dev/null +++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.ondeviceintelligence; + + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.PersistableBundle; + +import androidx.annotation.IntDef; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Exception type to be used for errors related to on-device intelligence system service with + * appropriate error code. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public class OnDeviceIntelligenceException extends Exception { + + public static final int PROCESSING_ERROR_UNKNOWN = 1; + + /** Request passed contains bad data for e.g. format. */ + public static final int PROCESSING_ERROR_BAD_DATA = 2; + + /** Bad request for inputs. */ + public static final int PROCESSING_ERROR_BAD_REQUEST = 3; + + /** Whole request was classified as not safe, and no response will be generated. */ + public static final int PROCESSING_ERROR_REQUEST_NOT_SAFE = 4; + + /** Underlying processing encountered an error and failed to compute results. */ + public static final int PROCESSING_ERROR_COMPUTE_ERROR = 5; + + /** Encountered an error while performing IPC */ + public static final int PROCESSING_ERROR_IPC_ERROR = 6; + + /** Request was cancelled either by user signal or by the underlying implementation. */ + public static final int PROCESSING_ERROR_CANCELLED = 7; + + /** Underlying processing in the remote implementation is not available. */ + public static final int PROCESSING_ERROR_NOT_AVAILABLE = 8; + + /** The service is currently busy. Callers should retry with exponential backoff. */ + public static final int PROCESSING_ERROR_BUSY = 9; + + /** Something went wrong with safety classification service. */ + public static final int PROCESSING_ERROR_SAFETY_ERROR = 10; + + /** Response generated was classified unsafe. */ + public static final int PROCESSING_ERROR_RESPONSE_NOT_SAFE = 11; + + /** Request is too large to be processed. */ + public static final int PROCESSING_ERROR_REQUEST_TOO_LARGE = 12; + + /** Inference suspended so that higher-priority inference can run. */ + public static final int PROCESSING_ERROR_SUSPENDED = 13; + + /** + * Underlying processing encountered an internal error, like a violated precondition + * . + */ + public static final int PROCESSING_ERROR_INTERNAL = 14; + + /** + * The processing was not able to be passed on to the remote implementation, as the + * service + * was unavailable. + */ + public static final int PROCESSING_ERROR_SERVICE_UNAVAILABLE = 15; + /** + * Error code returned when the OnDeviceIntelligenceManager service is unavailable. + */ + public static final int ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE = 100; + + /** + * The connection to remote service failed and the processing state could not be updated. + */ + public static final int PROCESSING_UPDATE_STATUS_CONNECTION_FAILED = 200; + + + /** + * Error code associated with the on-device intelligence failure. + * + * @hide + */ + @IntDef( + value = { + PROCESSING_ERROR_UNKNOWN, + PROCESSING_ERROR_BAD_DATA, + PROCESSING_ERROR_BAD_REQUEST, + PROCESSING_ERROR_REQUEST_NOT_SAFE, + PROCESSING_ERROR_COMPUTE_ERROR, + PROCESSING_ERROR_IPC_ERROR, + PROCESSING_ERROR_CANCELLED, + PROCESSING_ERROR_NOT_AVAILABLE, + PROCESSING_ERROR_BUSY, + PROCESSING_ERROR_SAFETY_ERROR, + PROCESSING_ERROR_RESPONSE_NOT_SAFE, + PROCESSING_ERROR_REQUEST_TOO_LARGE, + PROCESSING_ERROR_SUSPENDED, + PROCESSING_ERROR_INTERNAL, + PROCESSING_ERROR_SERVICE_UNAVAILABLE, + ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + PROCESSING_UPDATE_STATUS_CONNECTION_FAILED + }, open = true) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @interface OnDeviceIntelligenceError { + } + + private final int mErrorCode; + private final PersistableBundle mErrorParams; + + /** Returns the error code of the exception. */ + public int getErrorCode() { + return mErrorCode; + } + + /** Returns the error params of the exception. */ + @NonNull + public PersistableBundle getErrorParams() { + return mErrorParams; + } + + /** + * Creates a new OnDeviceIntelligenceException with the specified error code, error message and + * error params. + * + * @param errorCode The error code. + * @param errorMessage The error message. + * @param errorParams The error params. + */ + public OnDeviceIntelligenceException( + @OnDeviceIntelligenceError int errorCode, @NonNull String errorMessage, + @NonNull PersistableBundle errorParams) { + super(errorMessage); + this.mErrorCode = errorCode; + this.mErrorParams = errorParams; + } + + /** + * Creates a new OnDeviceIntelligenceException with the specified error code and error params. + * + * @param errorCode The error code. + * @param errorParams The error params. + */ + public OnDeviceIntelligenceException( + @OnDeviceIntelligenceError int errorCode, + @NonNull PersistableBundle errorParams) { + this.mErrorCode = errorCode; + this.mErrorParams = errorParams; + } + + /** + * Creates a new OnDeviceIntelligenceException with the specified error code and error message. + * + * @param errorCode The error code. + * @param errorMessage The error message. + */ + public OnDeviceIntelligenceException( + @OnDeviceIntelligenceError int errorCode, @NonNull String errorMessage) { + super(errorMessage); + this.mErrorCode = errorCode; + this.mErrorParams = new PersistableBundle(); + } + + /** + * Creates a new OnDeviceIntelligenceException with the specified error code. + * + * @param errorCode The error code. + */ + public OnDeviceIntelligenceException( + @OnDeviceIntelligenceError int errorCode) { + this.mErrorCode = errorCode; + this.mErrorParams = new PersistableBundle(); + } +} diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java index d195c4d52c22..a465e3cbb6ec 100644 --- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java +++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java @@ -28,6 +28,7 @@ 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; @@ -36,6 +37,7 @@ import android.os.OutcomeReceiver; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; +import android.system.OsConstants; import androidx.annotation.IntDef; @@ -63,7 +65,7 @@ import java.util.function.LongConsumer; @SystemApi @SystemService(Context.ON_DEVICE_INTELLIGENCE_SERVICE) @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) -public class OnDeviceIntelligenceManager { +public final class OnDeviceIntelligenceManager { /** * @hide */ @@ -118,14 +120,13 @@ public class OnDeviceIntelligenceManager { @Nullable @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public String getRemoteServicePackageName() { - String serviceConfigValue = mContext.getResources().getString( - R.string.config_defaultOnDeviceSandboxedInferenceService); - ComponentName componentName = ComponentName.unflattenFromString(serviceConfigValue); - if (componentName != null) { - return componentName.getPackageName(); + String result; + try{ + result = mService.getRemoteServicePackageName(); + } catch (RemoteException e){ + throw e.rethrowFromSystemServer(); } - - return null; + return result; } /** @@ -139,7 +140,7 @@ public class OnDeviceIntelligenceManager { public void getFeature( int featureId, @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull OutcomeReceiver<Feature, OnDeviceIntelligenceManagerException> featureReceiver) { + @NonNull OutcomeReceiver<Feature, OnDeviceIntelligenceException> featureReceiver) { try { IFeatureCallback callback = new IFeatureCallback.Stub() { @@ -154,7 +155,7 @@ public class OnDeviceIntelligenceManager { PersistableBundle errorParams) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( () -> featureReceiver.onError( - new OnDeviceIntelligenceManagerException( + new OnDeviceIntelligenceException( errorCode, errorMessage, errorParams)))); } }; @@ -173,7 +174,7 @@ public class OnDeviceIntelligenceManager { @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void listFeatures( @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull OutcomeReceiver<List<Feature>, OnDeviceIntelligenceManagerException> featureListReceiver) { + @NonNull OutcomeReceiver<List<Feature>, OnDeviceIntelligenceException> featureListReceiver) { try { IListFeaturesCallback callback = new IListFeaturesCallback.Stub() { @@ -188,7 +189,7 @@ public class OnDeviceIntelligenceManager { PersistableBundle errorParams) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( () -> featureListReceiver.onError( - new OnDeviceIntelligenceManagerException( + new OnDeviceIntelligenceException( errorCode, errorMessage, errorParams)))); } }; @@ -211,7 +212,7 @@ public class OnDeviceIntelligenceManager { @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeatureDetails(@NonNull Feature feature, @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull OutcomeReceiver<FeatureDetails, OnDeviceIntelligenceManagerException> featureDetailsReceiver) { + @NonNull OutcomeReceiver<FeatureDetails, OnDeviceIntelligenceException> featureDetailsReceiver) { try { IFeatureDetailsCallback callback = new IFeatureDetailsCallback.Stub() { @@ -226,7 +227,7 @@ public class OnDeviceIntelligenceManager { PersistableBundle errorParams) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( () -> featureDetailsReceiver.onError( - new OnDeviceIntelligenceManagerException(errorCode, + new OnDeviceIntelligenceException(errorCode, errorMessage, errorParams)))); } }; @@ -243,9 +244,8 @@ public class OnDeviceIntelligenceManager { * * Note: If a feature was already requested for downloaded previously, the onDownloadFailed * callback would be invoked with {@link DownloadCallback#DOWNLOAD_FAILURE_STATUS_DOWNLOADING}. - * In such cases, clients should query the feature status via {@link #getFeatureStatus} to - * check - * on the feature's download status. + * In such cases, clients should query the feature status via {@link #getFeatureDetails} to + * check on the feature's download status. * * @param feature feature to request download for. * @param callback callback to populate updates about download status. @@ -284,7 +284,7 @@ public class OnDeviceIntelligenceManager { @Override public void onDownloadCompleted(PersistableBundle downloadParams) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( - () -> onDownloadCompleted(downloadParams))); + () -> callback.onDownloadCompleted(downloadParams))); } }; @@ -305,7 +305,8 @@ public class OnDeviceIntelligenceManager { * provided {@link Feature}. * * @param feature feature associated with the request. - * @param request request that contains the content data and associated params. + * @param request request and associated params represented by the Bundle + * data. * @param outcomeReceiver callback to populate the token info or exception in case of * failure. * @param cancellationSignal signal to invoke cancellation on the operation in the remote @@ -313,11 +314,11 @@ public class OnDeviceIntelligenceManager { * @param callbackExecutor executor to run the callback on. */ @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) - public void requestTokenInfo(@NonNull Feature feature, @NonNull Content request, + public void requestTokenInfo(@NonNull Feature feature, @NonNull @InferenceParams Bundle request, @Nullable CancellationSignal cancellationSignal, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull OutcomeReceiver<TokenInfo, - OnDeviceIntelligenceManagerException> outcomeReceiver) { + OnDeviceIntelligenceException> outcomeReceiver) { try { ITokenInfoCallback callback = new ITokenInfoCallback.Stub() { @Override @@ -331,7 +332,7 @@ public class OnDeviceIntelligenceManager { PersistableBundle errorParams) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( () -> outcomeReceiver.onError( - new OnDeviceIntelligenceManagerProcessingException( + new OnDeviceIntelligenceException( errorCode, errorMessage, errorParams)))); } }; @@ -357,30 +358,30 @@ public class OnDeviceIntelligenceManager { * failure. * * @param feature feature associated with the request. - * @param request request that contains the Content data and - * associated params. + * @param request request and associated params represented by the Bundle + * data. * @param requestType type of request being sent for processing the content. * @param cancellationSignal signal to invoke cancellation. * @param processingSignal signal to send custom signals in the * remote implementation. * @param callbackExecutor executor to run the callback on. - * @param responseCallback callback to populate the response content and + * @param processingCallback callback to populate the response content and * associated params. */ @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) - public void processRequest(@NonNull Feature feature, @Nullable Content request, + public void processRequest(@NonNull Feature feature, @NonNull @InferenceParams Bundle request, @RequestType int requestType, @Nullable CancellationSignal cancellationSignal, @Nullable ProcessingSignal processingSignal, @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull ProcessingOutcomeReceiver responseCallback) { + @NonNull ProcessingCallback processingCallback) { try { IResponseCallback callback = new IResponseCallback.Stub() { @Override - public void onSuccess(Content result) { + public void onSuccess(@InferenceParams Bundle result) { Binder.withCleanCallingIdentity(() -> { - callbackExecutor.execute(() -> responseCallback.onResult(result)); + callbackExecutor.execute(() -> processingCallback.onResult(result)); }); } @@ -388,16 +389,16 @@ public class OnDeviceIntelligenceManager { public void onFailure(int errorCode, String errorMessage, PersistableBundle errorParams) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( - () -> responseCallback.onError( - new OnDeviceIntelligenceManagerProcessingException( + () -> processingCallback.onError( + new OnDeviceIntelligenceException( errorCode, errorMessage, errorParams)))); } @Override - public void onDataAugmentRequest(@NonNull Content content, + public void onDataAugmentRequest(@NonNull @InferenceParams Bundle request, @NonNull RemoteCallback contentCallback) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( - () -> responseCallback.onDataAugmentRequest(content, result -> { + () -> processingCallback.onDataAugmentRequest(request, result -> { Bundle bundle = new Bundle(); bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, result); callbackExecutor.execute(() -> contentCallback.sendResult(bundle)); @@ -430,15 +431,15 @@ public class OnDeviceIntelligenceManager { * Variation of {@link #processRequest} that asynchronously processes a request in a * streaming * fashion, where new content is pushed to caller in chunks via the - * {@link StreamedProcessingOutcomeReceiver#onNewContent}. After the streaming is complete, - * the service should call {@link StreamedProcessingOutcomeReceiver#onResult} and can optionally - * populate the complete the full response {@link Content} as part of the callback in cases - * when the final response contains an enhanced aggregation of the Contents already + * {@link StreamingProcessingCallback#onPartialResult}. After the streaming is complete, + * the service should call {@link StreamingProcessingCallback#onResult} and can optionally + * populate the complete the full response {@link Bundle} as part of the callback in cases + * when the final response contains an enhanced aggregation of the contents already * streamed. * * @param feature feature associated with the request. - * @param request request that contains the Content data and associated - * params. + * @param request request and associated params represented by the Bundle + * data. * @param requestType type of request being sent for processing the content. * @param cancellationSignal signal to invoke cancellation. * @param processingSignal signal to send custom signals in the @@ -448,27 +449,27 @@ public class OnDeviceIntelligenceManager { * @param callbackExecutor executor to run the callback on. */ @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) - public void processRequestStreaming(@NonNull Feature feature, @Nullable Content request, + public void processRequestStreaming(@NonNull Feature feature, @NonNull @InferenceParams Bundle request, @RequestType int requestType, @Nullable CancellationSignal cancellationSignal, @Nullable ProcessingSignal processingSignal, @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull StreamedProcessingOutcomeReceiver streamingResponseCallback) { + @NonNull StreamingProcessingCallback streamingProcessingCallback) { try { IStreamingResponseCallback callback = new IStreamingResponseCallback.Stub() { @Override - public void onNewContent(Content result) { + public void onNewContent(@InferenceParams Bundle result) { Binder.withCleanCallingIdentity(() -> { callbackExecutor.execute( - () -> streamingResponseCallback.onNewContent(result)); + () -> streamingProcessingCallback.onPartialResult(result)); }); } @Override - public void onSuccess(Content result) { + public void onSuccess(@InferenceParams Bundle result) { Binder.withCleanCallingIdentity(() -> { callbackExecutor.execute( - () -> streamingResponseCallback.onResult(result)); + () -> streamingProcessingCallback.onResult(result)); }); } @@ -477,18 +478,18 @@ public class OnDeviceIntelligenceManager { PersistableBundle errorParams) { Binder.withCleanCallingIdentity(() -> { callbackExecutor.execute( - () -> streamingResponseCallback.onError( - new OnDeviceIntelligenceManagerProcessingException( + () -> streamingProcessingCallback.onError( + new OnDeviceIntelligenceException( errorCode, errorMessage, errorParams))); }); } @Override - public void onDataAugmentRequest(@NonNull Content content, + public void onDataAugmentRequest(@NonNull @InferenceParams Bundle content, @NonNull RemoteCallback contentCallback) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( - () -> streamingResponseCallback.onDataAugmentRequest(content, + () -> streamingProcessingCallback.onDataAugmentRequest(content, contentResponse -> { Bundle bundle = new Bundle(); bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, @@ -519,7 +520,7 @@ public class OnDeviceIntelligenceManager { } - /** Request inference with provided Content and Params. */ + /** Request inference with provided Bundle and Params. */ public static final int REQUEST_TYPE_INFERENCE = 0; /** @@ -530,7 +531,7 @@ public class OnDeviceIntelligenceManager { */ public static final int REQUEST_TYPE_PREPARE = 1; - /** Request Embeddings of the passed-in Content. */ + /** Request Embeddings of the passed-in Bundle. */ public static final int REQUEST_TYPE_EMBEDDINGS = 2; /** @@ -547,154 +548,30 @@ public class OnDeviceIntelligenceManager { public @interface RequestType { } - - /** - * Exception type to be populated in callbacks to the methods under - * {@link OnDeviceIntelligenceManager}. - */ - public static class OnDeviceIntelligenceManagerException extends Exception { - /** - * Error code returned when the OnDeviceIntelligenceManager service is unavailable. - */ - public static final int ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE = 1000; - - /** - * Error code to be used for on device intelligence manager failures. - * - * @hide - */ - @IntDef( - value = { - ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE - }, open = true) - @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) - @interface OnDeviceIntelligenceManagerErrorCode { - } - - private final int mErrorCode; - private final PersistableBundle errorParams; - - public OnDeviceIntelligenceManagerException( - @OnDeviceIntelligenceManagerErrorCode int errorCode, @NonNull String errorMessage, - @NonNull PersistableBundle errorParams) { - super(errorMessage); - this.mErrorCode = errorCode; - this.errorParams = errorParams; - } - - public OnDeviceIntelligenceManagerException( - @OnDeviceIntelligenceManagerErrorCode int errorCode, - @NonNull PersistableBundle errorParams) { - this.mErrorCode = errorCode; - this.errorParams = errorParams; - } - - public int getErrorCode() { - return mErrorCode; - } - - @NonNull - public PersistableBundle getErrorParams() { - return errorParams; - } - } - /** - * Exception type to be populated in callbacks to the methods under - * {@link OnDeviceIntelligenceManager#processRequest} or - * {@link OnDeviceIntelligenceManager#processRequestStreaming} . + * {@link Bundle}s annotated with this type will be validated that they are in-effect read-only + * when passed to inference service via Binder IPC. Following restrictions apply : + * <ul> + * <li> Any primitive types or their collections can be added as usual.</li> + * <li>IBinder objects should *not* be added.</li> + * <li>Parcelable data which has no active-objects, should be added as + * {@link Bundle#putByteArray}</li> + * <li>Parcelables have active-objects, only following types will be allowed</li> + * <ul> + * <li>{@link Bitmap} set as {@link Bitmap#setImmutable()}</li> + * <li>{@link android.database.CursorWindow}</li> + * <li>{@link android.os.ParcelFileDescriptor} opened in + * {@link android.os.ParcelFileDescriptor#MODE_READ_ONLY}</li> + * <li>{@link android.os.SharedMemory} set to {@link OsConstants#PROT_READ}</li> + * </ul> + * </ul> + * + * In all other scenarios the system-server might throw a + * {@link android.os.BadParcelableException} if the Bundle validation fails. + * + * @hide */ - public static class OnDeviceIntelligenceManagerProcessingException extends - OnDeviceIntelligenceManagerException { - - public static final int PROCESSING_ERROR_UNKNOWN = 1; - - /** Request passed contains bad data for e.g. format. */ - public static final int PROCESSING_ERROR_BAD_DATA = 2; - - /** Bad request for inputs. */ - public static final int PROCESSING_ERROR_BAD_REQUEST = 3; - - /** Whole request was classified as not safe, and no response will be generated. */ - public static final int PROCESSING_ERROR_REQUEST_NOT_SAFE = 4; - - /** Underlying processing encountered an error and failed to compute results. */ - public static final int PROCESSING_ERROR_COMPUTE_ERROR = 5; - - /** Encountered an error while performing IPC */ - public static final int PROCESSING_ERROR_IPC_ERROR = 6; - - /** Request was cancelled either by user signal or by the underlying implementation. */ - public static final int PROCESSING_ERROR_CANCELLED = 7; - - /** Underlying processing in the remote implementation is not available. */ - public static final int PROCESSING_ERROR_NOT_AVAILABLE = 8; - - /** The service is currently busy. Callers should retry with exponential backoff. */ - public static final int PROCESSING_ERROR_BUSY = 9; - - /** Something went wrong with safety classification service. */ - public static final int PROCESSING_ERROR_SAFETY_ERROR = 10; - - /** Response generated was classified unsafe. */ - public static final int PROCESSING_ERROR_RESPONSE_NOT_SAFE = 11; - - /** Request is too large to be processed. */ - public static final int PROCESSING_ERROR_REQUEST_TOO_LARGE = 12; - - /** Inference suspended so that higher-priority inference can run. */ - public static final int PROCESSING_ERROR_SUSPENDED = 13; - - /** - * Underlying processing encountered an internal error, like a violated precondition - * . - */ - public static final int PROCESSING_ERROR_INTERNAL = 14; - - /** - * The processing was not able to be passed on to the remote implementation, as the - * service - * was unavailable. - */ - public static final int PROCESSING_ERROR_SERVICE_UNAVAILABLE = 15; - - /** - * Error code of failed processing request. - * - * @hide - */ - @IntDef( - value = { - PROCESSING_ERROR_UNKNOWN, - PROCESSING_ERROR_BAD_DATA, - PROCESSING_ERROR_BAD_REQUEST, - PROCESSING_ERROR_REQUEST_NOT_SAFE, - PROCESSING_ERROR_COMPUTE_ERROR, - PROCESSING_ERROR_IPC_ERROR, - PROCESSING_ERROR_CANCELLED, - PROCESSING_ERROR_NOT_AVAILABLE, - PROCESSING_ERROR_BUSY, - PROCESSING_ERROR_SAFETY_ERROR, - PROCESSING_ERROR_RESPONSE_NOT_SAFE, - PROCESSING_ERROR_REQUEST_TOO_LARGE, - PROCESSING_ERROR_SUSPENDED, - PROCESSING_ERROR_INTERNAL, - PROCESSING_ERROR_SERVICE_UNAVAILABLE - }, open = true) - @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) - @interface ProcessingError { - } - - public OnDeviceIntelligenceManagerProcessingException( - @ProcessingError int errorCode, @NonNull String errorMessage, - @NonNull PersistableBundle errorParams) { - super(errorCode, errorMessage, errorParams); - } - - public OnDeviceIntelligenceManagerProcessingException( - @ProcessingError int errorCode, - @NonNull PersistableBundle errorParams) { - super(errorCode, errorParams); - } + @Target({ElementType.PARAMETER, ElementType.FIELD}) + public @interface InferenceParams { } } diff --git a/core/java/android/app/ondeviceintelligence/ProcessingCallback.java b/core/java/android/app/ondeviceintelligence/ProcessingCallback.java new file mode 100644 index 000000000000..4d936ea45c54 --- /dev/null +++ b/core/java/android/app/ondeviceintelligence/ProcessingCallback.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams; + +import java.util.function.Consumer; + +/** + * Callback to populate the processed response or any error that occurred during the + * request processing. This callback also provides a method to request additional data to be + * augmented to the request-processing, using the partial response that was already + * processed in the remote implementation. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public interface ProcessingCallback { + /** + * Invoked when request has been processed and result is ready to be propagated to the + * caller. + * + * @param result Response to be passed as a result. + */ + void onResult(@NonNull @InferenceParams Bundle result); + + /** + * Called when the request processing fails. The failure details are indicated by the + * {@link OnDeviceIntelligenceException} passed as an argument to this method. + * + * @param error An exception with more details about the error that occurred. + */ + void onError(@NonNull OnDeviceIntelligenceException error); + + /** + * Callback to be invoked in cases where the remote service needs to perform retrieval or + * transformation operations based on a partially processed request, in order to augment the + * final response, by using the additional context sent via this callback. + * + * @param processedContent The content payload that should be used to augment ongoing request. + * @param contentConsumer The augmentation data that should be sent to remote + * service for further processing a request. Bundle passed in here is + * expected to be non-null or EMPTY when there is no response. + */ + default void onDataAugmentRequest( + @NonNull @InferenceParams Bundle processedContent, + @NonNull Consumer<Bundle> contentConsumer) { + contentConsumer.accept(Bundle.EMPTY); + } +} diff --git a/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java b/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java deleted file mode 100644 index b0b6e1948cf0..000000000000 --- a/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.app.ondeviceintelligence; - -import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; - -import android.annotation.FlaggedApi; -import android.annotation.NonNull; -import android.annotation.SystemApi; -import android.os.OutcomeReceiver; - -import java.util.function.Consumer; - -/** - * Response Callback to populate the processed response or any error that occurred during the - * request processing. This callback also provides a method to request additional data to be - * augmented to the request-processing, using the partial {@link Content} that was already - * processed in the remote implementation. - * - * @hide - */ -@SystemApi -@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) -public interface ProcessingOutcomeReceiver extends - OutcomeReceiver<Content, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> { - /** - * Callback to be invoked in cases where the remote service needs to perform retrieval or - * transformation operations based on a partially processed request, in order to augment the - * final response, by using the additional context sent via this callback. - * - * @param content The content payload that should be used to augment ongoing request. - * @param contentConsumer The augmentation data that should be sent to remote - * service for further processing a request. - */ - default void onDataAugmentRequest(@NonNull Content content, - @NonNull Consumer<Content> contentConsumer) { - contentConsumer.accept(null); - } -} diff --git a/core/java/android/app/ondeviceintelligence/StreamedProcessingOutcomeReceiver.java b/core/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java index ac2b0329496c..41f1807a9b3d 100644 --- a/core/java/android/app/ondeviceintelligence/StreamedProcessingOutcomeReceiver.java +++ b/core/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java @@ -21,19 +21,21 @@ import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.os.Bundle; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams; /** - * Streaming variant of outcome receiver to populate response while processing a given request, - * possibly in chunks to provide a async processing behaviour to the caller. + * Streaming variant of {@link ProcessingCallback} to populate response while processing a given + * request, possibly in chunks to provide a async processing behaviour to the caller. * * @hide */ @SystemApi @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) -public interface StreamedProcessingOutcomeReceiver extends ProcessingOutcomeReceiver { +public interface StreamingProcessingCallback extends ProcessingCallback { /** - * Callback that would be invoked when a part of the response i.e. some {@link Content} is - * already processed and needs to be passed onto the caller. + * Callback that would be invoked when a part of the response i.e. some response is + * already processed, and needs to be passed onto the caller. */ - void onNewContent(@NonNull Content content); + void onPartialResult(@NonNull @InferenceParams Bundle partialResult); } diff --git a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl index c4f653cdfed7..9d46b559ce37 100644 --- a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl +++ b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl @@ -25,5 +25,5 @@ parcelable CameraSessionConfig CameraMetadataNative sessionParameter; int sessionTemplateId; int sessionType; - int colorSpace; + int colorSpace = -1; } diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index 5b7f8bb00f25..6d9b51cbd003 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -348,7 +348,23 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes cameraOutput.setTimestampBase(OutputConfiguration.TIMESTAMP_BASE_SENSOR); cameraOutput.setReadoutTimestampEnabled(false); cameraOutput.setPhysicalCameraId(output.physicalCameraId); - cameraOutput.setDynamicRangeProfile(output.dynamicRangeProfile); + if (Flags.extension10Bit()) { + boolean validDynamicRangeProfile = false; + for (long profile = DynamicRangeProfiles.STANDARD; + profile < DynamicRangeProfiles.PUBLIC_MAX; profile <<= 1) { + if (output.dynamicRangeProfile == profile) { + validDynamicRangeProfile = true; + break; + } + } + if (validDynamicRangeProfile) { + cameraOutput.setDynamicRangeProfile(output.dynamicRangeProfile); + } else { + Log.e(TAG, "Extension configured dynamic range profile " + + output.dynamicRangeProfile + + " is not valid, using default DynamicRangeProfile.STANDARD"); + } + } outputList.add(cameraOutput); mCameraConfigMap.put(cameraOutput.getSurface(), output); } @@ -363,9 +379,15 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes SessionConfiguration sessionConfiguration = new SessionConfiguration(sessionType, outputList, new CameraExtensionUtils.HandlerExecutor(mHandler), new SessionStateHandler()); - if (sessionConfig.colorSpace != ColorSpaceProfiles.UNSPECIFIED) { - sessionConfiguration.setColorSpace( - ColorSpace.Named.values()[sessionConfig.colorSpace]); + if (Flags.extension10Bit()) { + if (sessionConfig.colorSpace >= 0 + && sessionConfig.colorSpace < ColorSpace.Named.values().length) { + sessionConfiguration.setColorSpace( + ColorSpace.Named.values()[sessionConfig.colorSpace]); + } else { + Log.e(TAG, "Extension configured color space " + sessionConfig.colorSpace + + " is not valid, using default unspecified color space"); + } } if ((sessionConfig.sessionParameter != null) && (!sessionConfig.sessionParameter.isEmpty())) { diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl index 73257ed7680a..799c7545968e 100644 --- a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl +++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl @@ -20,7 +20,6 @@ import android.app.ondeviceintelligence.IStreamingResponseCallback; import android.app.ondeviceintelligence.IResponseCallback; import android.app.ondeviceintelligence.ITokenInfoCallback; import android.app.ondeviceintelligence.IProcessingSignal; -import android.app.ondeviceintelligence.Content; import android.app.ondeviceintelligence.Feature; import android.os.ICancellationSignal; import android.os.PersistableBundle; @@ -35,12 +34,12 @@ import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; */ oneway interface IOnDeviceSandboxedInferenceService { void registerRemoteStorageService(in IRemoteStorageService storageService); - void requestTokenInfo(int callerUid, in Feature feature, in Content request, in ICancellationSignal cancellationSignal, + void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, in ICancellationSignal cancellationSignal, in ITokenInfoCallback tokenInfoCallback); - void processRequest(int callerUid, in Feature feature, in Content request, in int requestType, + void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal, in IResponseCallback callback); - void processRequestStreaming(int callerUid, in Feature feature, in Content request, in int requestType, + void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal, in IStreamingResponseCallback callback); void updateProcessingState(in Bundle processingState, diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java index fce3689bb8b3..27e862868858 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java @@ -32,7 +32,9 @@ import android.app.ondeviceintelligence.IDownloadCallback; import android.app.ondeviceintelligence.IFeatureCallback; import android.app.ondeviceintelligence.IFeatureDetailsCallback; import android.app.ondeviceintelligence.IListFeaturesCallback; +import android.app.ondeviceintelligence.OnDeviceIntelligenceException; import android.app.ondeviceintelligence.OnDeviceIntelligenceManager; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams; import android.content.Intent; import android.os.Binder; import android.os.Bundle; @@ -48,14 +50,8 @@ import android.util.Slog; import com.android.internal.infra.AndroidFuture; -import androidx.annotation.IntDef; - import java.io.File; import java.io.FileNotFoundException; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.List; import java.util.Map; import java.util.Objects; @@ -232,9 +228,9 @@ public abstract class OnDeviceIntelligenceService extends Service { * @param callbackExecutor executor to the run status callback on. * @param statusReceiver receiver to get status of the update state operation. */ - public final void updateProcessingState(@NonNull Bundle processingState, + public final void updateProcessingState(@NonNull @InferenceParams Bundle processingState, @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull OutcomeReceiver<PersistableBundle, OnDeviceUpdateProcessingException> statusReceiver) { + @NonNull OutcomeReceiver<PersistableBundle, OnDeviceIntelligenceException> statusReceiver) { Objects.requireNonNull(callbackExecutor); if (mRemoteProcessingService == null) { throw new IllegalStateException("Remote processing service is unavailable."); @@ -254,7 +250,7 @@ public abstract class OnDeviceIntelligenceService extends Service { public void onFailure(int errorCode, String errorMessage) { Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( () -> statusReceiver.onError( - new OnDeviceUpdateProcessingException( + new OnDeviceIntelligenceException( errorCode, errorMessage)))); } }); @@ -265,7 +261,7 @@ public abstract class OnDeviceIntelligenceService extends Service { } private OutcomeReceiver<Feature, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> wrapFeatureCallback( + OnDeviceIntelligenceException> wrapFeatureCallback( IFeatureCallback featureCallback) { return new OutcomeReceiver<>() { @Override @@ -279,7 +275,7 @@ public abstract class OnDeviceIntelligenceService extends Service { @Override public void onError( - @NonNull OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException exception) { + @NonNull OnDeviceIntelligenceException exception) { try { featureCallback.onFailure(exception.getErrorCode(), exception.getMessage(), exception.getErrorParams()); @@ -291,7 +287,7 @@ public abstract class OnDeviceIntelligenceService extends Service { } private OutcomeReceiver<List<Feature>, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> wrapListFeaturesCallback( + OnDeviceIntelligenceException> wrapListFeaturesCallback( IListFeaturesCallback listFeaturesCallback) { return new OutcomeReceiver<>() { @Override @@ -305,7 +301,7 @@ public abstract class OnDeviceIntelligenceService extends Service { @Override public void onError( - @NonNull OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException exception) { + @NonNull OnDeviceIntelligenceException exception) { try { listFeaturesCallback.onFailure(exception.getErrorCode(), exception.getMessage(), exception.getErrorParams()); @@ -317,7 +313,7 @@ public abstract class OnDeviceIntelligenceService extends Service { } private OutcomeReceiver<FeatureDetails, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> wrapFeatureDetailsCallback( + OnDeviceIntelligenceException> wrapFeatureDetailsCallback( IFeatureDetailsCallback featureStatusCallback) { return new OutcomeReceiver<>() { @Override @@ -331,7 +327,7 @@ public abstract class OnDeviceIntelligenceService extends Service { @Override public void onError( - @NonNull OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException exception) { + @NonNull OnDeviceIntelligenceException exception) { try { featureStatusCallback.onFailure(exception.getErrorCode(), exception.getMessage(), exception.getErrorParams()); @@ -444,7 +440,7 @@ public abstract class OnDeviceIntelligenceService extends Service { */ public abstract void onGetFeatureDetails(int callerUid, @NonNull Feature feature, @NonNull OutcomeReceiver<FeatureDetails, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> featureDetailsCallback); + OnDeviceIntelligenceException> featureDetailsCallback); /** @@ -455,7 +451,7 @@ public abstract class OnDeviceIntelligenceService extends Service { */ public abstract void onGetFeature(int callerUid, int featureId, @NonNull OutcomeReceiver<Feature, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> featureCallback); + OnDeviceIntelligenceException> featureCallback); /** * List all features which are available in the remote implementation. The implementation might @@ -465,7 +461,7 @@ public abstract class OnDeviceIntelligenceService extends Service { * @param listFeaturesCallback callback to populate the features list. */ public abstract void onListFeatures(int callerUid, @NonNull OutcomeReceiver<List<Feature>, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> listFeaturesCallback); + OnDeviceIntelligenceException> listFeaturesCallback); /** * Provides a long value representing the version of the remote implementation processing @@ -474,60 +470,4 @@ public abstract class OnDeviceIntelligenceService extends Service { * @param versionConsumer consumer to populate the version. */ public abstract void onGetVersion(@NonNull LongConsumer versionConsumer); - - - /** - * Exception type to be populated when calls to {@link #updateProcessingState} fail. - */ - public static class OnDeviceUpdateProcessingException extends - OnDeviceIntelligenceServiceException { - /** - * The connection to remote service failed and the processing state could not be updated. - */ - public static final int PROCESSING_UPDATE_STATUS_CONNECTION_FAILED = 1; - - - /** - * @hide - */ - @IntDef(value = { - PROCESSING_UPDATE_STATUS_CONNECTION_FAILED - }, open = true) - @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER, - ElementType.FIELD}) - @Retention(RetentionPolicy.SOURCE) - public @interface ErrorCode { - } - - public OnDeviceUpdateProcessingException(@ErrorCode int errorCode) { - super(errorCode); - } - - public OnDeviceUpdateProcessingException(@ErrorCode int errorCode, - @NonNull String errorMessage) { - super(errorCode, errorMessage); - } - } - - /** - * Exception type to be used for surfacing errors to service implementation. - */ - public abstract static class OnDeviceIntelligenceServiceException extends Exception { - private final int mErrorCode; - - public OnDeviceIntelligenceServiceException(int errorCode) { - this.mErrorCode = errorCode; - } - - public OnDeviceIntelligenceServiceException(int errorCode, - @NonNull String errorMessage) { - super(errorMessage); - this.mErrorCode = errorCode; - } - - public int getErrorCode() { - return mErrorCode; - } - - } } diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java index 7f7f9c28c60e..d943c80e525b 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java @@ -27,20 +27,21 @@ import android.annotation.SdkConstant; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.Service; -import android.app.ondeviceintelligence.Content; +import android.app.ondeviceintelligence.OnDeviceIntelligenceException; +import android.os.Bundle; import android.app.ondeviceintelligence.Feature; import android.app.ondeviceintelligence.IProcessingSignal; import android.app.ondeviceintelligence.IResponseCallback; import android.app.ondeviceintelligence.IStreamingResponseCallback; import android.app.ondeviceintelligence.ITokenInfoCallback; import android.app.ondeviceintelligence.OnDeviceIntelligenceManager; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams; import android.app.ondeviceintelligence.ProcessingSignal; -import android.app.ondeviceintelligence.ProcessingOutcomeReceiver; -import android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver; +import android.app.ondeviceintelligence.ProcessingCallback; +import android.app.ondeviceintelligence.StreamingProcessingCallback; import android.app.ondeviceintelligence.TokenInfo; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.HandlerExecutor; @@ -51,7 +52,6 @@ import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; -import android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceUpdateProcessingException; import android.util.Log; import android.util.Slog; @@ -121,7 +121,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { } @Override - public void requestTokenInfo(int callerUid, Feature feature, Content request, + public void requestTokenInfo(int callerUid, Feature feature, Bundle request, ICancellationSignal cancellationSignal, ITokenInfoCallback tokenInfoCallback) { Objects.requireNonNull(feature); @@ -134,7 +134,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { } @Override - public void processRequestStreaming(int callerUid, Feature feature, Content request, + public void processRequestStreaming(int callerUid, Feature feature, Bundle request, int requestType, ICancellationSignal cancellationSignal, IProcessingSignal processingSignal, IStreamingResponseCallback callback) { @@ -151,7 +151,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { } @Override - public void processRequest(int callerUid, Feature feature, Content request, + public void processRequest(int callerUid, Feature feature, Bundle request, int requestType, ICancellationSignal cancellationSignal, IProcessingSignal processingSignal, IResponseCallback callback) { @@ -198,18 +198,17 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @NonNull public abstract void onTokenInfoRequest( int callerUid, @NonNull Feature feature, - @NonNull Content request, + @NonNull @InferenceParams Bundle request, @Nullable CancellationSignal cancellationSignal, - @NonNull OutcomeReceiver<TokenInfo, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> callback); + @NonNull OutcomeReceiver<TokenInfo, OnDeviceIntelligenceException> callback); /** * 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 StreamedProcessingOutcomeReceiver#onNewContent} to continuously - * provide partial Content results for the caller to utilize. Optionally the implementation can - * provide the complete response in the {@link StreamedProcessingOutcomeReceiver#onResult} upon + * 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. * * @param callerUid UID of the caller that initiated this call chain. @@ -225,11 +224,11 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @NonNull public abstract void onProcessRequestStreaming( int callerUid, @NonNull Feature feature, - @Nullable Content request, + @NonNull @InferenceParams Bundle request, @OnDeviceIntelligenceManager.RequestType int requestType, @Nullable CancellationSignal cancellationSignal, @Nullable ProcessingSignal processingSignal, - @NonNull StreamedProcessingOutcomeReceiver callback); + @NonNull StreamingProcessingCallback callback); /** * Invoked when caller provides a request for a particular feature to be processed in one shot @@ -251,11 +250,11 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @NonNull public abstract void onProcessRequest( int callerUid, @NonNull Feature feature, - @Nullable Content request, + @NonNull @InferenceParams Bundle request, @OnDeviceIntelligenceManager.RequestType int requestType, @Nullable CancellationSignal cancellationSignal, @Nullable ProcessingSignal processingSignal, - @NonNull ProcessingOutcomeReceiver callback); + @NonNull ProcessingCallback callback); /** @@ -267,9 +266,9 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { * @param callback callback to populate the update status and if there are params * associated with the status. */ - public abstract void onUpdateProcessingState(@NonNull Bundle processingState, + public abstract void onUpdateProcessingState(@NonNull @InferenceParams Bundle processingState, @NonNull OutcomeReceiver<PersistableBundle, - OnDeviceUpdateProcessingException> callback); + OnDeviceIntelligenceException> callback); /** @@ -344,7 +343,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { /** * Returns the {@link Executor} to use for incoming IPC from request sender into your service * implementation. For e.g. see - * {@link ProcessingOutcomeReceiver#onDataAugmentRequest(Content, + * {@link ProcessingCallback#onDataAugmentRequest(Bundle, * Consumer)} where we use the executor to populate the consumer. * <p> * Override this method in your {@link OnDeviceSandboxedInferenceService} implementation to @@ -380,13 +379,13 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { }); } - private ProcessingOutcomeReceiver wrapResponseCallback( + private ProcessingCallback wrapResponseCallback( IResponseCallback callback) { - return new ProcessingOutcomeReceiver() { + return new ProcessingCallback() { @Override - public void onResult(@androidx.annotation.NonNull Content response) { + public void onResult(@androidx.annotation.NonNull Bundle result) { try { - callback.onSuccess(response); + callback.onSuccess(result); } catch (RemoteException e) { Slog.e(TAG, "Error sending result: " + e); } @@ -394,7 +393,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @Override public void onError( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException exception) { + OnDeviceIntelligenceException exception) { try { callback.onFailure(exception.getErrorCode(), exception.getMessage(), exception.getErrorParams()); @@ -404,8 +403,8 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { } @Override - public void onDataAugmentRequest(@NonNull Content content, - @NonNull Consumer<Content> contentCallback) { + public void onDataAugmentRequest(@NonNull Bundle content, + @NonNull Consumer<Bundle> contentCallback) { try { callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback)); @@ -416,22 +415,22 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { }; } - private StreamedProcessingOutcomeReceiver wrapStreamingResponseCallback( + private StreamingProcessingCallback wrapStreamingResponseCallback( IStreamingResponseCallback callback) { - return new StreamedProcessingOutcomeReceiver() { + return new StreamingProcessingCallback() { @Override - public void onNewContent(@androidx.annotation.NonNull Content content) { + public void onPartialResult(@androidx.annotation.NonNull Bundle partialResult) { try { - callback.onNewContent(content); + callback.onNewContent(partialResult); } catch (RemoteException e) { Slog.e(TAG, "Error sending result: " + e); } } @Override - public void onResult(@androidx.annotation.NonNull Content response) { + public void onResult(@androidx.annotation.NonNull Bundle result) { try { - callback.onSuccess(response); + callback.onSuccess(result); } catch (RemoteException e) { Slog.e(TAG, "Error sending result: " + e); } @@ -439,7 +438,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @Override public void onError( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException exception) { + OnDeviceIntelligenceException exception) { try { callback.onFailure(exception.getErrorCode(), exception.getMessage(), exception.getErrorParams()); @@ -449,8 +448,8 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { } @Override - public void onDataAugmentRequest(@NonNull Content content, - @NonNull Consumer<Content> contentCallback) { + public void onDataAugmentRequest(@NonNull Bundle content, + @NonNull Consumer<Bundle> contentCallback) { try { callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback)); @@ -462,13 +461,13 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { } private RemoteCallback wrapRemoteCallback( - @androidx.annotation.NonNull Consumer<Content> contentCallback) { + @androidx.annotation.NonNull Consumer<Bundle> contentCallback) { return new RemoteCallback( result -> { if (result != null) { getCallbackExecutor().execute(() -> contentCallback.accept( result.getParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, - Content.class))); + Bundle.class))); } else { getCallbackExecutor().execute( () -> contentCallback.accept(null)); @@ -476,8 +475,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { }); } - private OutcomeReceiver<TokenInfo, - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapTokenInfoCallback( + private OutcomeReceiver<TokenInfo, OnDeviceIntelligenceException> wrapTokenInfoCallback( ITokenInfoCallback tokenInfoCallback) { return new OutcomeReceiver<>() { @Override @@ -491,7 +489,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @Override public void onError( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException exception) { + OnDeviceIntelligenceException exception) { try { tokenInfoCallback.onFailure(exception.getErrorCode(), exception.getMessage(), exception.getErrorParams()); @@ -503,7 +501,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { } @NonNull - private static OutcomeReceiver<PersistableBundle, OnDeviceUpdateProcessingException> wrapOutcomeReceiver( + private static OutcomeReceiver<PersistableBundle, OnDeviceIntelligenceException> wrapOutcomeReceiver( IProcessingUpdateStatusCallback callback) { return new OutcomeReceiver<>() { @Override @@ -518,7 +516,7 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @Override public void onError( - @androidx.annotation.NonNull OnDeviceUpdateProcessingException error) { + @androidx.annotation.NonNull OnDeviceIntelligenceException error) { try { callback.onFailure(error.getErrorCode(), error.getMessage()); } catch (RemoteException e) { diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index f68fcab94952..aff1d4a4ee12 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -119,3 +119,10 @@ flag { is_fixed_read_only: true bug: "324676775" } + +flag { + name: "handwriting_cursor_position" + namespace: "text" + description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph." + bug: "323376217" +} diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 66655fca8fc3..29c83509dbf2 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -16,6 +16,8 @@ package android.view; +import static com.android.text.flags.Flags.handwritingCursorPosition; + import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -557,7 +559,8 @@ public class HandwritingInitiator { } private void requestFocusWithoutReveal(View view) { - if (view instanceof EditText editText && !mState.mStylusDownWithinEditorBounds) { + if (!handwritingCursorPosition() && view instanceof EditText editText + && !mState.mStylusDownWithinEditorBounds) { // If the stylus down point was inside the EditText's bounds, then the EditText will // automatically set its cursor position nearest to the stylus down point when it // gains focus. If the stylus down point was outside the EditText's bounds (within @@ -576,6 +579,17 @@ public class HandwritingInitiator { } else { view.requestFocus(); } + if (handwritingCursorPosition() && view instanceof EditText editText) { + // Move the cursor to the end of the paragraph closest to the stylus down point. + view.getLocationInWindow(mTempLocation); + int line = editText.getLineAtCoordinate(mState.mStylusDownY - mTempLocation[1]); + int paragraphEnd = TextUtils.indexOf(editText.getText(), '\n', + editText.getLayout().getLineStart(line)); + if (paragraphEnd < 0) { + paragraphEnd = editText.getText().length(); + } + editText.setSelection(paragraphEnd); + } } /** diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 139ebc38706e..d40eedaea77b 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -2112,6 +2112,17 @@ public class Editor { } } + boolean shouldDrawHighlightsOnTop = highContrastTextSmallTextRect() + && canvas.isHighContrastTextEnabled(); + + // If high contrast text is drawing background rectangles behind the text, those cover up + // the cursor and correction highlighter etc. So just draw the text first, then draw the + // others on top of the text. If high contrast text isn't enabled: draw text last, as usual. + if (shouldDrawHighlightsOnTop) { + drawLayout(canvas, layout, highlightPaths, highlightPaints, selectionHighlight, + selectionHighlightPaint, cursorOffsetVertical, shouldDrawHighlightsOnTop); + } + if (mCorrectionHighlighter != null) { mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); } @@ -2136,9 +2147,19 @@ public class Editor { mInsertModeController.onDraw(canvas); } + if (!shouldDrawHighlightsOnTop) { + drawLayout(canvas, layout, highlightPaths, highlightPaints, selectionHighlight, + selectionHighlightPaint, cursorOffsetVertical, shouldDrawHighlightsOnTop); + } + } + + private void drawLayout(Canvas canvas, Layout layout, List<Path> highlightPaths, + List<Paint> highlightPaints, Path selectionHighlight, Paint selectionHighlightPaint, + int cursorOffsetVertical, boolean shouldDrawHighlightsOnTop) { if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) { drawHardwareAccelerated(canvas, layout, highlightPaths, highlightPaints, - selectionHighlight, selectionHighlightPaint, cursorOffsetVertical); + selectionHighlight, selectionHighlightPaint, cursorOffsetVertical, + shouldDrawHighlightsOnTop); } else { layout.draw(canvas, highlightPaths, highlightPaints, selectionHighlight, selectionHighlightPaint, cursorOffsetVertical); @@ -2147,14 +2168,13 @@ public class Editor { private void drawHardwareAccelerated(Canvas canvas, Layout layout, List<Path> highlightPaths, List<Paint> highlightPaints, - Path selectionHighlight, Paint selectionHighlightPaint, int cursorOffsetVertical) { + Path selectionHighlight, Paint selectionHighlightPaint, int cursorOffsetVertical, + boolean shouldDrawHighlightsOnTop) { final long lineRange = layout.getLineRangeForDraw(canvas); int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); if (lastLine < 0) return; - boolean shouldDrawHighlightsOnTop = highContrastTextSmallTextRect() - && canvas.isHighContrastTextEnabled(); if (!shouldDrawHighlightsOnTop) { layout.drawWithoutText(canvas, highlightPaths, highlightPaints, selectionHighlight, diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 172146215257..0373539c44ea 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -15490,8 +15490,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return x; } + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - int getLineAtCoordinate(float y) { + public int getLineAtCoordinate(float y) { y -= getTotalPaddingTop(); // Clamp the position to inside of the view. y = Math.max(0.0f, y); diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index 7b8cdff8e20b..7e77f150b63b 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -24,6 +24,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.view.SurfaceControl; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -111,7 +112,8 @@ public final class TaskFragmentOperation implements Parcelable { /** * Creates a decor surface in the parent Task of the TaskFragment. The created decor surface * will be provided in {@link TaskFragmentTransaction#TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED} - * event callback. + * event callback. The decor surface can be used to draw the divider between TaskFragments or + * other decorations. */ public static final int OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE = 14; @@ -135,6 +137,15 @@ public final class TaskFragmentOperation implements Parcelable { */ public static final int OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH = 17; + /** + * Sets whether the decor surface will be boosted. When not boosted, the decor surface is placed + * below any TaskFragments in untrusted mode or any activities with uid different from the + * TaskFragmentOrganizer uid and just above its owner TaskFragment; when boosted, the decor + * surface is placed above all the non-boosted windows in the Task, the content of these + * non-boosted windows will be hidden and inputs are disabled. + */ + public static final int OP_TYPE_SET_DECOR_SURFACE_BOOSTED = 18; + @IntDef(prefix = { "OP_TYPE_" }, value = { OP_TYPE_UNKNOWN, OP_TYPE_CREATE_TASK_FRAGMENT, @@ -155,6 +166,7 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_SET_DIM_ON_TASK, OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH, + OP_TYPE_SET_DECOR_SURFACE_BOOSTED, }) @Retention(RetentionPolicy.SOURCE) public @interface OperationType {} @@ -186,12 +198,18 @@ public final class TaskFragmentOperation implements Parcelable { private final boolean mMoveToBottomIfClearWhenLaunch; + private final boolean mBooleanValue; + + @Nullable + private final SurfaceControl.Transaction mSurfaceTransaction; + private TaskFragmentOperation(@OperationType int opType, @Nullable TaskFragmentCreationParams taskFragmentCreationParams, @Nullable IBinder activityToken, @Nullable Intent activityIntent, @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken, @Nullable TaskFragmentAnimationParams animationParams, - boolean isolatedNav, boolean dimOnTask, boolean moveToBottomIfClearWhenLaunch) { + boolean isolatedNav, boolean dimOnTask, boolean moveToBottomIfClearWhenLaunch, + boolean booleanValue, @Nullable SurfaceControl.Transaction surfaceTransaction) { mOpType = opType; mTaskFragmentCreationParams = taskFragmentCreationParams; mActivityToken = activityToken; @@ -202,6 +220,8 @@ public final class TaskFragmentOperation implements Parcelable { mIsolatedNav = isolatedNav; mDimOnTask = dimOnTask; mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch; + mBooleanValue = booleanValue; + mSurfaceTransaction = surfaceTransaction; } private TaskFragmentOperation(Parcel in) { @@ -215,6 +235,8 @@ public final class TaskFragmentOperation implements Parcelable { mIsolatedNav = in.readBoolean(); mDimOnTask = in.readBoolean(); mMoveToBottomIfClearWhenLaunch = in.readBoolean(); + mBooleanValue = in.readBoolean(); + mSurfaceTransaction = in.readTypedObject(SurfaceControl.Transaction.CREATOR); } @Override @@ -229,6 +251,8 @@ public final class TaskFragmentOperation implements Parcelable { dest.writeBoolean(mIsolatedNav); dest.writeBoolean(mDimOnTask); dest.writeBoolean(mMoveToBottomIfClearWhenLaunch); + dest.writeBoolean(mBooleanValue); + dest.writeTypedObject(mSurfaceTransaction, flags); } @NonNull @@ -324,6 +348,22 @@ public final class TaskFragmentOperation implements Parcelable { return mMoveToBottomIfClearWhenLaunch; } + /** Returns the boolean value for this operation. */ + public boolean getBooleanValue() { + return mBooleanValue; + } + + /** + * Returns {@link SurfaceControl.Transaction} associated with this operation. Currently, this is + * only used by {@link TaskFragmentOperation#OP_TYPE_SET_DECOR_SURFACE_BOOSTED} to specify a + * {@link SurfaceControl.Transaction} that should be applied together with the transaction to + * change the decor surface layers. + */ + @Nullable + public SurfaceControl.Transaction getSurfaceTransaction() { + return mSurfaceTransaction; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder(); @@ -349,6 +389,10 @@ public final class TaskFragmentOperation implements Parcelable { sb.append(", isolatedNav=").append(mIsolatedNav); sb.append(", dimOnTask=").append(mDimOnTask); sb.append(", moveToBottomIfClearWhenLaunch=").append(mMoveToBottomIfClearWhenLaunch); + sb.append(", booleanValue=").append(mBooleanValue); + if (mSurfaceTransaction != null) { + sb.append(", surfaceTransaction=").append(mSurfaceTransaction); + } sb.append('}'); return sb.toString(); @@ -358,7 +402,7 @@ public final class TaskFragmentOperation implements Parcelable { public int hashCode() { return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask, - mMoveToBottomIfClearWhenLaunch); + mMoveToBottomIfClearWhenLaunch, mBooleanValue, mSurfaceTransaction); } @Override @@ -376,7 +420,9 @@ public final class TaskFragmentOperation implements Parcelable { && Objects.equals(mAnimationParams, other.mAnimationParams) && mIsolatedNav == other.mIsolatedNav && mDimOnTask == other.mDimOnTask - && mMoveToBottomIfClearWhenLaunch == other.mMoveToBottomIfClearWhenLaunch; + && mMoveToBottomIfClearWhenLaunch == other.mMoveToBottomIfClearWhenLaunch + && mBooleanValue == other.mBooleanValue + && Objects.equals(mSurfaceTransaction, other.mSurfaceTransaction); } @Override @@ -414,6 +460,11 @@ public final class TaskFragmentOperation implements Parcelable { private boolean mMoveToBottomIfClearWhenLaunch; + private boolean mBooleanValue; + + @Nullable + private SurfaceControl.Transaction mSurfaceTransaction; + /** * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}. */ @@ -505,13 +556,37 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Sets the boolean value for this operation. + * TODO(b/327338038) migrate other boolean values to use shared mBooleanValue + */ + @NonNull + public Builder setBooleanValue(boolean booleanValue) { + mBooleanValue = booleanValue; + return this; + } + + /** + * Sets {@link SurfaceControl.Transaction} associated with this operation. Currently, this + * is only used by {@link TaskFragmentOperation#OP_TYPE_SET_DECOR_SURFACE_BOOSTED} to + * specify a {@link SurfaceControl.Transaction} that should be applied together with the + * transaction to change the decor surface layers. + */ + @NonNull + public Builder setSurfaceTransaction( + @Nullable SurfaceControl.Transaction surfaceTransaction) { + mSurfaceTransaction = surfaceTransaction; + return this; + } + + /** * Constructs the {@link TaskFragmentOperation}. */ @NonNull public TaskFragmentOperation build() { return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams, - mIsolatedNav, mDimOnTask, mMoveToBottomIfClearWhenLaunch); + mIsolatedNav, mDimOnTask, mMoveToBottomIfClearWhenLaunch, mBooleanValue, + mSurfaceTransaction); } } } diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index b60b806f3444..a5c962412024 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -23,6 +23,8 @@ import static android.view.MotionEvent.ACTION_UP; import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import static android.view.stylus.HandwritingTestUtil.createView; +import static com.android.text.flags.Flags.handwritingCursorPosition; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeFalse; @@ -129,6 +131,7 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_when_stylusMoveOnce_withinHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); + when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); mHandwritingInitiator.onInputConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; @@ -148,9 +151,51 @@ public class HandwritingInitiatorTest { // After IMM.startHandwriting is triggered, onTouchEvent should return true for ACTION_MOVE // events so that the events are not dispatched to the view tree. assertThat(onTouchEventResult2).isTrue(); - // Since the stylus down point was inside the TextView's bounds, the handwriting initiator - // does not need to set the cursor position. - verify(mTestView1, never()).setSelection(anyInt()); + if (handwritingCursorPosition()) { + // Cursor is placed at the end of the text. + verify(mTestView1).setSelection(5); + } else { + // Since the stylus down point was inside the TextView's bounds, the handwriting + // initiator does not need to set the cursor position. + verify(mTestView1, never()).setSelection(anyInt()); + } + } + + @Test + public void onTouchEvent_startHandwriting_multipleParagraphs() { + // End of line 0 is offset 10, end of line 1 is offset 20, end of line 2 is offset 30, end + // of line 3 is offset 40. + mTestView1.setText("line 0 \nline 1 \nline 2 \nline 3 "); + mTestView1.layout(0, 0, 500, 500); + when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); + when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(2); + + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + final int x1 = (sHwArea1.left + sHwArea1.right) / 2; + final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; + MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); + boolean onTouchEventResult1 = mHandwritingInitiator.onTouchEvent(stylusEvent1); + + final int x2 = x1 + mHandwritingSlop * 2; + final int y2 = y1; + + MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); + boolean onTouchEventResult2 = mHandwritingInitiator.onTouchEvent(stylusEvent2); + + // Stylus movement within HandwritingArea should trigger IMM.startHandwriting once. + verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); + assertThat(onTouchEventResult1).isFalse(); + // After IMM.startHandwriting is triggered, onTouchEvent should return true for ACTION_MOVE + // events so that the events are not dispatched to the view tree. + assertThat(onTouchEventResult2).isTrue(); + if (handwritingCursorPosition()) { + // Cursor is placed at the end of the paragraph containing line 2. + verify(mTestView1).setSelection(30); + } else { + // Since the stylus down point was inside the TextView's bounds, the handwriting + // initiator does not need to set the cursor position. + verify(mTestView1, never()).setSelection(anyInt()); + } } @Test @@ -197,6 +242,7 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_when_stylusMove_withinExtendedHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); + when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); if (!mInitiateWithoutConnection) { mHandwritingInitiator.onInputConnectionCreated(mTestView1); @@ -214,9 +260,14 @@ public class HandwritingInitiatorTest { // Stylus movement within extended HandwritingArea should trigger IMM.startHandwriting once. verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); - // Since the stylus down point was outside the TextView's bounds, the handwriting initiator - // sets the cursor position. - verify(mTestView1).setSelection(4); + if (handwritingCursorPosition()) { + // Cursor is placed at the end of the text. + verify(mTestView1).setSelection(5); + } else { + // Since the stylus down point was outside the TextView's bounds, the handwriting + // initiator sets the cursor position. + verify(mTestView1).setSelection(4); + } } @Test @@ -246,6 +297,8 @@ public class HandwritingInitiatorTest { public void onTouchEvent_startHandwriting_servedViewUpdate_stylusMoveInExtendedHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); + when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); + // The stylus down point is between mTestView1 and mTestView2, but it is within the // extended handwriting area of both views. It is closer to mTestView1. final int x1 = sHwArea1.right + HW_BOUNDS_OFFSETS_RIGHT_PX / 2; @@ -278,9 +331,14 @@ public class HandwritingInitiatorTest { // Handwriting is started for this view since the stylus down point is closest to this // view. verify(mHandwritingInitiator).startHandwriting(mTestView1); - // Since the stylus down point was outside the TextView's bounds, the handwriting initiator - // sets the cursor position. - verify(mTestView1).setSelection(4); + if (handwritingCursorPosition()) { + // Cursor is placed at the end of the text. + verify(mTestView1).setSelection(5); + } else { + // Since the stylus down point was outside the TextView's bounds, the handwriting + // initiator sets the cursor position. + verify(mTestView1).setSelection(4); + } } diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 0f12438613cf..749f0e12aa03 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -553,6 +553,8 @@ applications that come with the platform <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/> <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases --> <permission name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE"/> + <!-- Permission required for CTS test - OnDeviceIntelligenceManagerTest --> + <permission name="android.permission.USE_ON_DEVICE_INTELLIGENCE" /> <!-- Permission required for CTS test - CtsTelephonyProviderTestCases --> <permission name="android.permission.WRITE_APN_SETTINGS"/> <!-- Permission required for GTS test - GtsStatsdHostTestCases --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 23bdd08e6b24..6524c96fb21a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -449,17 +449,21 @@ public class BubbleStackView extends FrameLayout @Override public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, - @NonNull MagnetizedObject draggedObject) { - if (draggedObject.getUnderlyingObject() instanceof View view) { + @NonNull MagnetizedObject<?> draggedObject) { + Object underlyingObject = draggedObject.getUnderlyingObject(); + if (underlyingObject instanceof View) { + View view = (View) underlyingObject; animateDismissBubble(view, true); } } @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, - @NonNull MagnetizedObject draggedObject, + @NonNull MagnetizedObject<?> draggedObject, float velX, float velY, boolean wasFlungOut) { - if (draggedObject.getUnderlyingObject() instanceof View view) { + Object underlyingObject = draggedObject.getUnderlyingObject(); + if (underlyingObject instanceof View) { + View view = (View) underlyingObject; animateDismissBubble(view, false); if (wasFlungOut) { @@ -474,7 +478,9 @@ public class BubbleStackView extends FrameLayout @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, @NonNull MagnetizedObject<?> draggedObject) { - if (draggedObject.getUnderlyingObject() instanceof View view) { + Object underlyingObject = draggedObject.getUnderlyingObject(); + if (underlyingObject instanceof View) { + View view = (View) underlyingObject; mExpandedAnimationController.dismissDraggedOutBubble( view /* bubble */, mDismissView.getHeight() /* translationYBy */, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index fb0ed1587055..6a3c8d2f599a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -138,9 +138,9 @@ public class DesktopModeVisualIndicator { @WindowConfiguration.WindowingMode int windowingMode, int captionHeight) { final Region region = new Region(); int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM - ? 2 * layout.stableInsets().top - : mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height); + ? mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height) + : 2 * layout.stableInsets().top; // A thin, short Rect at the top of the screen. if (windowingMode == WINDOWING_MODE_FREEFORM) { int fromFreeformWidth = mContext.getResources().getDimensionPixelSize( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index dd8c1a0f2e02..4a3130f3c54b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -78,6 +78,7 @@ import com.android.wm.shell.sysui.ShellSharedConstants import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.KtProtoLog +import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import java.io.PrintWriter @@ -960,7 +961,8 @@ class DesktopTasksController( } /** - * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area. + * Perform checks required on drag end. If indicator indicates a windowing mode change, perform + * that change. Otherwise, ensure bounds are up to date. * * @param taskInfo the task being dragged. * @param position position of surface when drag ends. @@ -971,7 +973,8 @@ class DesktopTasksController( taskInfo: RunningTaskInfo, position: Point, inputCoordinate: PointF, - taskBounds: Rect + taskBounds: Rect, + validDragArea: Rect ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return @@ -994,10 +997,21 @@ class DesktopTasksController( releaseVisualIndicator() snapToHalfScreen(taskInfo, SnapPosition.RIGHT) } - DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR -> { + // If task bounds are outside valid drag area, snap them inward and perform a + // transaction to set bounds. + if (DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( + taskBounds, validDragArea)) { + val wct = WindowContainerTransaction() + wct.setBounds(taskInfo.token, taskBounds) + transitions.startTransition(TRANSIT_CHANGE, wct, null) + } releaseVisualIndicator() } + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { + throw IllegalArgumentException("Should not be receiving TO_DESKTOP_INDICATOR for " + + "a freeform task.") + } } // A freeform drag-move ended, remove the indicator immediately. releaseVisualIndicator() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index b2eeea7048bc..c59a1b452303 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -19,9 +19,11 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_CHANGE; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; +import android.graphics.Rect; import android.os.Handler; import android.util.SparseArray; import android.view.Choreographer; @@ -186,7 +188,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { final FluidResizeTaskPositioner taskPositioner = new FluidResizeTaskPositioner(mTaskOrganizer, mTransitions, windowDecoration, - mDisplayController, 0 /* disallowedAreaForEndBoundsHeight */); + mDisplayController); final CaptionTouchEventListener touchEventListener = new CaptionTouchEventListener(taskInfo, taskPositioner); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); @@ -286,8 +288,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mDragPointerId = e.getPointerId(0); } final int dragPointerIdx = e.findPointerIndex(mDragPointerId); - mDragPositioningCallback.onDragPositioningEnd( + final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(newTaskBounds, + mWindowDecorByTaskId.get(mTaskId).calculateValidDragArea()); + if (newTaskBounds != taskInfo.configuration.windowConfiguration.getBounds()) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(taskInfo.token, newTaskBounds); + mTransitions.startTransition(TRANSIT_CHANGE, wct, null); + } final boolean wasDragging = mIsDragging; mIsDragging = false; return wasDragging; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 91e9601c6a27..9a48922fd238 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -16,6 +16,7 @@ package com.android.wm.shell.windowdecor; +import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; @@ -87,6 +88,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } @Override + @NonNull Rect calculateValidDragArea() { final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(), R.dimen.caption_left_buttons_width); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 98ff0eed9c11..918cefbee9f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -639,7 +639,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); mDesktopTasksController.onDragPositioningEnd(taskInfo, position, new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), - newTaskBounds); + newTaskBounds, decoration.calculateValidDragArea()); if (touchingButton && !mHasLongClicked) { // We need the input event to not be consumed here to end the ripple // effect on the touched button. We will reset drag state in the ensuing @@ -867,8 +867,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } if (mTransitionDragActive) { // Do not create an indicator at all if we're not past transition height. - if (ev.getRawY() < mContext.getResources().getDimensionPixelSize(com.android - .wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height) + DisplayLayout layout = mDisplayController + .getDisplayLayout(relevantDecor.mTaskInfo.displayId); + if (ev.getRawY() < 2 * layout.stableInsets().top && mMoveToDesktopAnimator == null) { return; } @@ -1086,18 +1087,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { windowDecoration.createResizeVeil(); final DragPositioningCallback dragPositioningCallback; - final int transitionAreaHeight = mContext.getResources().getDimensionPixelSize( - R.dimen.desktop_mode_fullscreen_from_desktop_height); if (!DesktopModeStatus.isVeiledResizeEnabled()) { dragPositioningCallback = new FluidResizeTaskPositioner( mTaskOrganizer, mTransitions, windowDecoration, mDisplayController, - mDragStartListener, mTransactionFactory, transitionAreaHeight); + mDragStartListener, mTransactionFactory); windowDecoration.setTaskDragResizer( (FluidResizeTaskPositioner) dragPositioningCallback); } else { dragPositioningCallback = new VeiledResizeTaskPositioner( mTaskOrganizer, windowDecoration, mDisplayController, - mDragStartListener, mTransitions, transitionAreaHeight); + mDragStartListener, mTransitions); windowDecoration.setTaskDragResizer( (VeiledResizeTaskPositioner) dragPositioningCallback); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index c9669a788994..4c9e17155625 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.windowingModeToString; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.WindowConfiguration.WindowingMode; import android.content.Context; @@ -499,6 +500,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Determine valid drag area for this task based on elements in the app chip. */ @Override + @NonNull Rect calculateValidDragArea() { final int appTextWidth = ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder).getAppNameTextWidth(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java index 5afbd54088d1..82c399ad8152 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java @@ -131,7 +131,7 @@ public class DragPositioningCallbackUtility { t.setPosition(decoration.mTaskSurface, repositionTaskBounds.left, repositionTaskBounds.top); } - private static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, + static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, PointF repositionStartPoint, float x, float y) { final float deltaX = x - repositionStartPoint.x; final float deltaY = y - repositionStartPoint.y; @@ -140,49 +140,32 @@ public class DragPositioningCallbackUtility { } /** - * Calculates the new position of the top edge of the task and returns true if it is below the - * disallowed area. - * - * @param disallowedAreaForEndBoundsHeight the height of the area that where the task positioner - * should not finalize the bounds using WCT#setBounds - * @param taskBoundsAtDragStart the bounds of the task on the first drag input event - * @param repositionStartPoint initial input coordinate - * @param y the y position of the motion event - * @return true if the top of the task is below the disallowed area + * If task bounds are outside of provided drag area, snap the bounds to be just inside the + * drag area. + * @param repositionTaskBounds bounds determined by task positioner + * @param validDragArea the area that task must be positioned inside + * @return whether bounds were modified */ - static boolean isBelowDisallowedArea(int disallowedAreaForEndBoundsHeight, - Rect taskBoundsAtDragStart, PointF repositionStartPoint, float y) { - final float deltaY = y - repositionStartPoint.y; - final float topPosition = taskBoundsAtDragStart.top + deltaY; - return topPosition > disallowedAreaForEndBoundsHeight; - } - - /** - * Updates repositionTaskBounds to the final bounds of the task after the drag is finished. If - * the bounds are outside of the valid drag area, the task is shifted back onto the edge of the - * valid drag area. - */ - static void onDragEnd(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, - PointF repositionStartPoint, float x, float y, Rect validDragArea) { - updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint, - x, y); - snapTaskBoundsIfNecessary(repositionTaskBounds, validDragArea); - } - - private static void snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) { + public static boolean snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) { // If we were never supplied a valid drag area, do not restrict movement. // Otherwise, we restrict deltas to keep task position inside the Rect. - if (validDragArea.width() == 0) return; + if (validDragArea.width() == 0) return false; + boolean result = false; if (repositionTaskBounds.left < validDragArea.left) { repositionTaskBounds.offset(validDragArea.left - repositionTaskBounds.left, 0); + result = true; } else if (repositionTaskBounds.left > validDragArea.right) { repositionTaskBounds.offset(validDragArea.right - repositionTaskBounds.left, 0); + result = true; } if (repositionTaskBounds.top < validDragArea.top) { repositionTaskBounds.offset(0, validDragArea.top - repositionTaskBounds.top); + result = true; } else if (repositionTaskBounds.top > validDragArea.bottom) { repositionTaskBounds.offset(0, validDragArea.bottom - repositionTaskBounds.top); + result = true; } + return result; } private static float getMinWidth(DisplayController displayController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java index 6bfc7cdcb33e..6f8b3d5aaaad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -60,9 +60,6 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mRepositionStartPoint = new PointF(); private final Rect mRepositionTaskBounds = new Rect(); - // If a task move (not resize) finishes with the positions y less than this value, do not - // finalize the bounds there using WCT#setBounds - private final int mDisallowedAreaForEndBoundsHeight; private boolean mHasDragResized; private boolean mIsResizingOrAnimatingResize; private int mCtrlType; @@ -70,11 +67,9 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, @Surface.Rotation private int mRotation; FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, Transitions transitions, - WindowDecoration windowDecoration, DisplayController displayController, - int disallowedAreaForEndBoundsHeight) { + WindowDecoration windowDecoration, DisplayController displayController) { this(taskOrganizer, transitions, windowDecoration, displayController, - dragStartListener -> {}, SurfaceControl.Transaction::new, - disallowedAreaForEndBoundsHeight); + dragStartListener -> {}, SurfaceControl.Transaction::new); } FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, @@ -82,15 +77,13 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, WindowDecoration windowDecoration, DisplayController displayController, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Supplier<SurfaceControl.Transaction> supplier, - int disallowedAreaForEndBoundsHeight) { + Supplier<SurfaceControl.Transaction> supplier) { mTaskOrganizer = taskOrganizer; mTransitions = transitions; mWindowDecoration = windowDecoration; mDisplayController = displayController; mDragStartListener = dragStartListener; mTransactionSupplier = supplier; - mDisallowedAreaForEndBoundsHeight = disallowedAreaForEndBoundsHeight; } @Override @@ -157,14 +150,10 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); } mDragResizeEndTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); - } else if (mCtrlType == CTRL_TYPE_UNDEFINED - && DragPositioningCallbackUtility.isBelowDisallowedArea( - mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint, - y)) { + } else if (mCtrlType == CTRL_TYPE_UNDEFINED) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds, - mTaskBoundsAtDragStart, mRepositionStartPoint, x, y, - mWindowDecoration.calculateValidDragArea()); + DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, + mTaskBoundsAtDragStart, mRepositionStartPoint, x, y); wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); mTransitions.startTransition(TRANSIT_CHANGE, wct, this); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 5c69d5542227..c12a93edcaf3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -54,9 +54,6 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mRepositionStartPoint = new PointF(); private final Rect mRepositionTaskBounds = new Rect(); - // If a task move (not resize) finishes with the positions y less than this value, do not - // finalize the bounds there using WCT#setBounds - private final int mDisallowedAreaForEndBoundsHeight; private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; private int mCtrlType; private boolean mIsResizingOrAnimatingResize; @@ -66,25 +63,22 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, DesktopModeWindowDecoration windowDecoration, DisplayController displayController, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Transitions transitions, - int disallowedAreaForEndBoundsHeight) { + Transitions transitions) { this(taskOrganizer, windowDecoration, displayController, dragStartListener, - SurfaceControl.Transaction::new, transitions, disallowedAreaForEndBoundsHeight); + SurfaceControl.Transaction::new, transitions); } public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, DesktopModeWindowDecoration windowDecoration, DisplayController displayController, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Supplier<SurfaceControl.Transaction> supplier, Transitions transitions, - int disallowedAreaForEndBoundsHeight) { + Supplier<SurfaceControl.Transaction> supplier, Transitions transitions) { mDesktopWindowDecoration = windowDecoration; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; mDragStartListener = dragStartListener; mTransactionSupplier = supplier; mTransitions = transitions; - mDisallowedAreaForEndBoundsHeight = disallowedAreaForEndBoundsHeight; } @Override @@ -151,13 +145,10 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, // won't be called. resetVeilIfVisible(); } - } else if (DragPositioningCallbackUtility.isBelowDisallowedArea( - mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint, - y)) { + } else { final WindowContainerTransaction wct = new WindowContainerTransaction(); - DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds, - mTaskBoundsAtDragStart, mRepositionStartPoint, x, y, - mDesktopWindowDecoration.calculateValidDragArea()); + DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, + mTaskBoundsAtDragStart, mRepositionStartPoint, x, y); wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); mTransitions.startTransition(TRANSIT_CHANGE, wct, this); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index 9703dce8bf53..bd39aa6ace42 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -68,17 +68,17 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { ) var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) - assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top)) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect( DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2, -50, DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2, - 2 * STABLE_INSETS.top)) + transitionHeight)) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT) - assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top)) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt index e60be7186b1e..e6fabcfec58a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -189,8 +189,9 @@ class DragPositioningCallbackUtilityTest { DISPLAY_BOUNDS.right - 100, DISPLAY_BOUNDS.bottom - 100) - DragPositioningCallbackUtility.onDragEnd(repositionTaskBounds, STARTING_BOUNDS, - startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat(), + DragPositioningCallbackUtility.updateTaskBounds(repositionTaskBounds, STARTING_BOUNDS, + startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat()) + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(repositionTaskBounds, validDragArea) assertThat(repositionTaskBounds.left).isEqualTo(validDragArea.left) assertThat(repositionTaskBounds.top).isEqualTo(validDragArea.bottom) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index de6903d9a06a..ce7b63322b4a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt @@ -125,8 +125,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { mockWindowDecoration, mockDisplayController, mockDragStartListener, - mockTransactionFactory, - DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT + mockTransactionFactory ) } @@ -576,31 +575,6 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { }) } - @Test - fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = STARTING_BOUNDS.right.toFloat() + 5 - val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - - taskPositioner.onDragPositioningEnd(newX, newY) - - verify(mockTransitions, never()).startTransition( - eq(WindowManager.TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) - }}, eq(taskPositioner)) - } - private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) && bounds == configuration.windowConfiguration.bounds @@ -656,70 +630,6 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_taskPositionedInStableBounds() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.left.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = STARTING_BOUNDS.left.toFloat() - val newY = STABLE_BOUNDS_LANDSCAPE.top.toFloat() - 5 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - verify(mockTransaction).setPosition(any(), eq(newX), eq(newY)) - - taskPositioner.onDragPositioningEnd( - newX, - newY - ) - // Verify task's top bound is set to stable bounds top since dragged outside stable bounds - // but not in disallowed end bounds area. - verify(mockTransitions).startTransition( - eq(WindowManager.TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds.top == - STABLE_BOUNDS_LANDSCAPE.top - }}, eq(taskPositioner)) - } - - @Test - fun testDragResize_drag_taskPositionedInValidDragArea() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.left.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = VALID_DRAG_AREA.left - 500f - val newY = VALID_DRAG_AREA.bottom + 500f - taskPositioner.onDragPositioningMove( - newX, - newY - ) - verify(mockTransaction).setPosition(any(), eq(newX), eq(newY)) - - taskPositioner.onDragPositioningEnd( - newX, - newY - ) - verify(mockTransitions).startTransition( - eq(WindowManager.TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds.top == - VALID_DRAG_AREA.bottom && - change.configuration.windowConfiguration.bounds.left == - VALID_DRAG_AREA.left - }}, eq(taskPositioner)) - } - - @Test fun testDragResize_drag_updatesStableBoundsOnRotate() { // Test landscape stable bounds performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 86253f35a51d..7f6e538f0bbf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -138,8 +138,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockDisplayController, mockDragStartListener, mockTransactionFactory, - mockTransitions, - DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT + mockTransitions ) } @@ -355,68 +354,6 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_taskPositionedInStableBounds() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.left.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = STARTING_BOUNDS.left.toFloat() - val newY = STABLE_BOUNDS_LANDSCAPE.top.toFloat() - 5 - taskPositioner.onDragPositioningMove( - newX, - newY - ) - verify(mockTransaction).setPosition(any(), eq(newX), eq(newY)) - - taskPositioner.onDragPositioningEnd( - newX, - newY - ) - // Verify task's top bound is set to stable bounds top since dragged outside stable bounds - // but not in disallowed end bounds area. - verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds.top == - STABLE_BOUNDS_LANDSCAPE.top }}, - eq(taskPositioner)) - } - - @Test - fun testDragResize_drag_taskPositionedInValidDragArea() { - taskPositioner.onDragPositioningStart( - CTRL_TYPE_UNDEFINED, // drag - STARTING_BOUNDS.left.toFloat(), - STARTING_BOUNDS.top.toFloat() - ) - - val newX = VALID_DRAG_AREA.left - 500f - val newY = VALID_DRAG_AREA.bottom + 500f - taskPositioner.onDragPositioningMove( - newX, - newY - ) - verify(mockTransaction).setPosition(any(), eq(newX), eq(newY)) - - taskPositioner.onDragPositioningEnd( - newX, - newY - ) - verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds.top == - VALID_DRAG_AREA.bottom && - change.configuration.windowConfiguration.bounds.left == - VALID_DRAG_AREA.left }}, - eq(taskPositioner)) - } - - @Test fun testDragResize_drag_updatesStableBoundsOnRotate() { // Test landscape stable bounds performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(), diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt index e79176bdebcf..56b1c2e3d5a4 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt @@ -56,7 +56,7 @@ fun SinglePasskeyScreen( headerContent = { SignInHeader( icon = entry.icon, - title = stringResource(R.string.use_password_title), + title = stringResource(R.string.use_passkey_title), ) }, accountContent = { diff --git a/packages/SettingsLib/DataStore/README.md b/packages/SettingsLib/DataStore/README.md new file mode 100644 index 000000000000..30cb9932f104 --- /dev/null +++ b/packages/SettingsLib/DataStore/README.md @@ -0,0 +1,164 @@ +# Datastore library + +This library aims to manage datastore in a consistent way. + +## Overview + +A datastore is required to extend the `BackupRestoreStorage` class and implement +either `Observable` or `KeyedObservable` interface, which enforces: + +- Backup and restore: Datastore should support + [data backup](https://developer.android.com/guide/topics/data/backup) to + preserve user experiences on a new device. +- Observer pattern: The + [observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) allows to + monitor data change in the datastore and + - trigger + [BackupManager.dataChanged](https://developer.android.com/reference/android/app/backup/BackupManager#dataChanged\(\)) + automatically. + - track data change event to log metrics. + - update internal state and take action. + +### Backup and restore + +The Android backup framework provides +[BackupAgentHelper](https://developer.android.com/reference/android/app/backup/BackupAgentHelper) +and +[BackupHelper](https://developer.android.com/reference/android/app/backup/BackupHelper) +to back up a datastore. However, there are several caveats when implement +`BackupHelper`: + +- performBackup: The data is updated incrementally but it is not well + documented. The `ParcelFileDescriptor` state parameters are normally ignored + and data is updated even there is no change. +- restoreEntity: The implementation must take care not to seek or close the + underlying data source, nor read more than size() bytes from the stream when + restore (see + [BackupDataInputStream](https://developer.android.com/reference/android/app/backup/BackupDataInputStream)). + It is possible a `BackupHelper` prevents other `BackupHelper`s from + restoring data. +- writeNewStateDescription: Existing implementations rarely notice that this + callback is invoked after all entities are restored, and check if necessary + data are all restored in `restoreEntity` (e.g. + [BatteryBackupHelper](https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/fuelgauge/BatteryBackupHelper.java;l=144;drc=cca804e1ed504e2d477be1e3db00fb881ca32736)), + which is not robust sometimes. + +This library provides more clear API and offers some improvements: + +- The implementation only needs to focus on the `BackupRestoreEntity` + interface. The `InputStream` of restore will ensure bounded data are read, + and close the stream will be no-op. +- The library computes checksum of the backup data automatically, so that + unchanged data will not be sent to Android backup system. +- Data compression is supported: + - ZIP best compression is enabled by default, no extra effort needs to be + taken. + - It is safe to switch between compression and no compression in future, + the backup data will add 1 byte header to recognize the codec. + - To support other compression algorithms, simply wrap over the + `InputStream` and `OutputStream`. Actually, the checksum is computed in + this way by + [CheckedInputStream](https://developer.android.com/reference/java/util/zip/CheckedInputStream) + and + [CheckedOutputStream](https://developer.android.com/reference/java/util/zip/CheckedOutputStream), + see `BackupRestoreStorage` implementation for more details. +- Enhanced forward compatibility for file is enabled: If a backup includes + data that didn't exist in earlier versions of the app, the data can still be + successfully restored in those older versions. This is achieved by extending + the `BackupRestoreFileStorage` class, and `BackupRestoreFileArchiver` will + treat each file as an entity and do the backup / restore. +- Manual `BackupManager.dataChanged` call is unnecessary now, the library will + do the invocation (see next section). + +### Observer pattern + +Manual `BackupManager.dataChanged` call is required by current backup framework. +In practice, it is found that `SharedPreferences` usages foget to invoke the +API. Besides, there are common use cases to log metrics when data is changed. +Consequently, observer pattern is employed to resolve the issues. + +If the datastore is key-value based (e.g. `SharedPreferences`), implements the +`KeyedObservable` interface to offer fine-grained observer. Otherwise, +implements `Observable`. The library provides thread-safe implementations +(`KeyedDataObservable` / `DataObservable`), and Kotlin delegation will be +helpful. + +Keep in mind that the implementation should call `KeyedObservable.notifyChange` +/ `Observable.notifyChange` whenever internal data is changed, so that the +registered observer will be notified properly. + +## Usage and example + +For `SharedPreferences` use case, leverage the `SharedPreferencesStorage`. To +back up other file based storage, extend the `BackupRestoreFileStorage` class. + +Here is an example of customized datastore, which has a string to back up: + +```kotlin +class MyDataStore : ObservableBackupRestoreStorage() { + // Another option is make it a StringEntity type and maintain a String field inside StringEntity + @Volatile // backup/restore happens on Binder thread + var data: String? = null + private set + + fun setData(data: String?) { + this.data = data + notifyChange(ChangeReason.UPDATE) + } + + override val name: String + get() = "MyData" + + override fun createBackupRestoreEntities(): List<BackupRestoreEntity> = + listOf(StringEntity("data")) + + private inner class StringEntity(override val key: String) : BackupRestoreEntity { + override fun backup( + backupContext: BackupContext, + outputStream: OutputStream, + ) = + if (data != null) { + outputStream.write(data!!.toByteArray(UTF_8)) + EntityBackupResult.UPDATE + } else { + EntityBackupResult.DELETE + } + + override fun restore(restoreContext: RestoreContext, inputStream: InputStream) { + data = String(inputStream.readAllBytes(), UTF_8) + // NOTE: The library will call notifyChange(ChangeReason.RESTORE) for you + } + } + + override fun onRestoreFinished() { + // TODO: Update state with the restored data. Use this callback instead "restore()" in case + // the restore action involves several entities. + // NOTE: The library will call notifyChange(ChangeReason.RESTORE) for you + } +} +``` + +In the application class: + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate(); + BackupRestoreStorageManager.getInstance(this).add(MyDataStore()); + } +} +``` + +In the custom `BackupAgentHelper` class: + +```kotlin +class MyBackupAgentHelper : BackupAgentHelper() { + override fun onCreate() { + BackupRestoreStorageManager.getInstance(this).addBackupAgentHelpers(this); + } + + override fun onRestoreFinished() { + BackupRestoreStorageManager.getInstance(this).onRestoreFinished(); + } +} +``` diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/VolumeControlProfileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/VolumeControlProfileTest.java index fe1529d11cd8..9c518de18582 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/VolumeControlProfileTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/VolumeControlProfileTest.java @@ -192,7 +192,7 @@ public class VolumeControlProfileTest { mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService); final Executor executor = (command -> new Thread(command).start()); - final BluetoothVolumeControl.Callback callback = (device, volumeOffset) -> {}; + final BluetoothVolumeControl.Callback callback = new BluetoothVolumeControl.Callback() {}; mProfile.registerCallback(executor, callback); verify(mService).registerCallback(executor, callback); @@ -200,7 +200,7 @@ public class VolumeControlProfileTest { @Test public void unregisterCallback_verifyIsCalled() { - final BluetoothVolumeControl.Callback callback = (device, volumeOffset) -> {}; + final BluetoothVolumeControl.Callback callback = new BluetoothVolumeControl.Callback() {}; mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService); mProfile.unregisterCallback(callback); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index eb2d13dc9eb5..43ea3ec3de4e 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -698,6 +698,9 @@ <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases --> <uses-permission android:name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE" /> + <!-- Permission required for CTS test - OnDeviceIntelligenceManagerTest --> + <uses-permission android:name="android.permission.USE_ON_DEVICE_INTELLIGENCE" /> + <!-- Permission required for CTS test - CallAudioInterceptionTest --> <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION" /> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 390daa52b498..1c9863087704 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -463,6 +463,13 @@ flag { } flag { + name: "enable_contextual_tips_frequency_cap" + description: "Enables frequency capping for contextual tips, e.g. 1x/day, 2x/week, 3x/lifetime." + namespace: "systemui" + bug: "322891421" +} + +flag { name: "enable_contextual_tips" description: "Enables showing contextual tips." namespace: "systemui" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index e683f34c4ea1..278bf9b545a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel import com.android.systemui.kosmos.testScope @@ -699,6 +700,25 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun alphaOnFullQsExpansion() = + testScope.runTest { + val viewState = ViewStateAccessor() + val alpha by collectLastValue(underTest.keyguardAlpha(viewState)) + + showLockscreenWithQSExpanded() + + // Alpha fades out as QS expands + shadeRepository.setQsExpansion(0.5f) + assertThat(alpha).isWithin(0.01f).of(0.5f) + shadeRepository.setQsExpansion(0.9f) + assertThat(alpha).isWithin(0.01f).of(0.1f) + + // Ensure that alpha is set back to 1f when QS is fully expanded + shadeRepository.setQsExpansion(1f) + assertThat(alpha).isEqualTo(1f) + } + + @Test fun shadeCollapseFadeIn() = testScope.runTest { val fadeIn by collectLastValue(underTest.shadeCollapseFadeIn) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt new file mode 100644 index 000000000000..183a58a495a3 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy + +import android.app.Notification +import android.platform.test.annotations.EnableFlags +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.settings.FakeGlobalSettings +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.invocation.InvocationOnMock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@SmallTest +@RunWithLooper +@RunWith(AndroidJUnit4::class) +@EnableFlags(NotificationThrottleHun.FLAG_NAME) +class AvalancheControllerTest : SysuiTestCase() { + + private val mAvalancheController = AvalancheController() + + // For creating mocks + @get:Rule var rule: MockitoRule = MockitoJUnit.rule() + @Mock private val runnableMock: Runnable? = null + + // For creating TestableHeadsUpManager + @Mock private val mAccessibilityMgr: AccessibilityManagerWrapper? = null + private val mUiEventLoggerFake = UiEventLoggerFake() + private val mLogger = Mockito.spy(HeadsUpManagerLogger(logcatLogBuffer())) + private val mGlobalSettings = FakeGlobalSettings() + private val mSystemClock = FakeSystemClock() + private val mExecutor = FakeExecutor(mSystemClock) + private var testableHeadsUpManager: BaseHeadsUpManager? = null + + @Before + fun setUp() { + // Use default non-a11y timeout + Mockito.`when`( + mAccessibilityMgr!!.getRecommendedTimeoutMillis( + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt() + ) + ) + .then { i: InvocationOnMock -> i.getArgument(0) } + + // Initialize TestableHeadsUpManager here instead of at declaration, when mocks will be null + testableHeadsUpManager = + TestableHeadsUpManager( + mContext, + mLogger, + mExecutor, + mGlobalSettings, + mSystemClock, + mAccessibilityMgr, + mUiEventLoggerFake, + mAvalancheController + ) + } + + private fun createHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry { + val entry = testableHeadsUpManager!!.createHeadsUpEntry() + + entry.setEntry( + NotificationEntryBuilder() + .setSbn(HeadsUpManagerTestUtil.createSbn(id, Notification.Builder(mContext, ""))) + .build() + ) + return entry + } + + @Test + fun testUpdate_isShowing_runsRunnable() { + // Entry is showing + val headsUpEntry = createHeadsUpEntry(id = 0) + mAvalancheController.headsUpEntryShowing = headsUpEntry + + // Update + mAvalancheController.update(headsUpEntry, runnableMock!!, "testLabel") + + // Runnable was run + Mockito.verify(runnableMock, Mockito.times(1)).run() + } + + @Test + fun testUpdate_noneShowingAndNotNext_showNow() { + val headsUpEntry = createHeadsUpEntry(id = 0) + + // None showing + mAvalancheController.headsUpEntryShowing = null + + // Entry is NOT next + mAvalancheController.clearNext() + + // Update + mAvalancheController.update(headsUpEntry, runnableMock!!, "testLabel") + + // Entry is showing now + Truth.assertThat(mAvalancheController.headsUpEntryShowing).isEqualTo(headsUpEntry) + } + + @Test + fun testUpdate_isNext_addsRunnable() { + // Another entry is already showing + val otherShowingEntry = createHeadsUpEntry(id = 0) + mAvalancheController.headsUpEntryShowing = otherShowingEntry + + // Entry is next + val headsUpEntry = createHeadsUpEntry(id = 1) + mAvalancheController.addToNext(headsUpEntry, runnableMock!!) + + // Entry has one Runnable + val runnableList: List<Runnable?>? = mAvalancheController.nextMap[headsUpEntry] + Truth.assertThat(runnableList).isNotNull() + Truth.assertThat(runnableList!!.size).isEqualTo(1) + + // Update + mAvalancheController.update(headsUpEntry, runnableMock, "testLabel") + + // Entry has two Runnables + Truth.assertThat(runnableList.size).isEqualTo(2) + } + + @Test + fun testUpdate_isNotNextWithOtherHunShowing_isNext() { + val headsUpEntry = createHeadsUpEntry(id = 0) + + // Another entry is already showing + val otherShowingEntry = createHeadsUpEntry(id = 1) + mAvalancheController.headsUpEntryShowing = otherShowingEntry + + // Entry is NOT next + mAvalancheController.clearNext() + + // Update + mAvalancheController.update(headsUpEntry, runnableMock!!, "testLabel") + + // Entry is next + Truth.assertThat(mAvalancheController.nextMap.containsKey(headsUpEntry)).isTrue() + } + + @Test + fun testDelete_isNext_removedFromNext_runnableNotRun() { + // Entry is next + val headsUpEntry = createHeadsUpEntry(id = 0) + mAvalancheController.addToNext(headsUpEntry, runnableMock!!) + + // Delete + mAvalancheController.delete(headsUpEntry, runnableMock, "testLabel") + + // Entry was removed from next + Truth.assertThat(mAvalancheController.nextMap.containsKey(headsUpEntry)).isFalse() + + // Runnable was not run + Mockito.verify(runnableMock, Mockito.times(0)).run() + } + + @Test + fun testDelete_wasDropped_removedFromDropSet() { + // Entry was dropped + val headsUpEntry = createHeadsUpEntry(id = 0) + mAvalancheController.debugDropSet.add(headsUpEntry) + + // Delete + mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel") + + // Entry was removed from dropSet + Truth.assertThat(mAvalancheController.debugDropSet.contains(headsUpEntry)).isFalse() + } + + @Test + fun testDelete_wasDropped_runnableNotRun() { + // Entry was dropped + val headsUpEntry = createHeadsUpEntry(id = 0) + mAvalancheController.debugDropSet.add(headsUpEntry) + + // Delete + mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel") + + // Runnable was not run + Mockito.verify(runnableMock, Mockito.times(0)).run() + } + + @Test + fun testDelete_isShowing_runnableRun() { + // Entry is showing + val headsUpEntry = createHeadsUpEntry(id = 0) + mAvalancheController.headsUpEntryShowing = headsUpEntry + + // Delete + mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel") + + // Runnable was run + Mockito.verify(runnableMock, Mockito.times(1)).run() + } + + @Test + fun testDelete_isShowing_showNext() { + // Entry is showing + val showingEntry = createHeadsUpEntry(id = 0) + mAvalancheController.headsUpEntryShowing = showingEntry + + // There's another entry waiting to show next + val nextEntry = createHeadsUpEntry(id = 1) + mAvalancheController.addToNext(nextEntry, runnableMock!!) + + // Delete + mAvalancheController.delete(showingEntry, runnableMock, "testLabel") + + // Next entry is shown + Truth.assertThat(mAvalancheController.headsUpEntryShowing).isEqualTo(nextEntry) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java index db4d42f4c864..830bcef55046 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java @@ -35,13 +35,10 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; import android.content.Intent; -import android.os.UserHandle; -import android.service.notification.StatusBarNotification; import android.testing.TestableLooper; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -77,6 +74,8 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { private UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); private final HeadsUpManagerLogger mLogger = spy(new HeadsUpManagerLogger(logcatLogBuffer())); + private AvalancheController mAvalancheController = new AvalancheController(); + @Mock private AccessibilityManagerWrapper mAccessibilityMgr; protected static final int TEST_MINIMUM_DISPLAY_TIME = 400; @@ -99,7 +98,7 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { private BaseHeadsUpManager createHeadsUpManager() { return new TestableHeadsUpManager(mContext, mLogger, mExecutor, mGlobalSettings, - mSystemClock, mAccessibilityMgr, mUiEventLoggerFake); + mSystemClock, mAccessibilityMgr, mUiEventLoggerFake, mAvalancheController); } private NotificationEntry createStickyEntry(int id) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java index c032d7cb06b2..61a79d897b0b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java @@ -28,8 +28,8 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.testing.TestableLooper; -import androidx.test.filters.SmallTest; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -76,6 +76,7 @@ public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest { @Mock private UiEventLogger mUiEventLogger; @Mock private JavaAdapter mJavaAdapter; @Mock private ShadeInteractor mShadeInteractor; + private AvalancheController mAvalancheController = new AvalancheController(); private static final class TestableHeadsUpManagerPhone extends HeadsUpManagerPhone { TestableHeadsUpManagerPhone( @@ -92,7 +93,8 @@ public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest { AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger, JavaAdapter javaAdapter, - ShadeInteractor shadeInteractor + ShadeInteractor shadeInteractor, + AvalancheController avalancheController ) { super( context, @@ -109,7 +111,8 @@ public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest { accessibilityManagerWrapper, uiEventLogger, javaAdapter, - shadeInteractor + shadeInteractor, + avalancheController ); mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME; mAutoDismissTime = TEST_AUTO_DISMISS_TIME; @@ -131,12 +134,15 @@ public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest { mAccessibilityManagerWrapper, mUiEventLogger, mJavaAdapter, - mShadeInteractor + mShadeInteractor, + mAvalancheController ); } @Before public void setUp() { + // TODO(b/315362456) create separate test with the flag disabled + // then modify this file to test with the flag enabled mSetFlagsRule.disableFlags(NotificationThrottleHun.FLAG_NAME); when(mShadeInteractor.isAnyExpanded()).thenReturn(StateFlowKt.MutableStateFlow(false)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java index 27476299cf18..d8f77f054b49 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java @@ -43,9 +43,10 @@ class TestableHeadsUpManager extends BaseHeadsUpManager { GlobalSettings globalSettings, SystemClock systemClock, AccessibilityManagerWrapper accessibilityManagerWrapper, - UiEventLogger uiEventLogger) { + UiEventLogger uiEventLogger, + AvalancheController avalancheController) { super(context, logger, mockExecutorHandler(executor), globalSettings, systemClock, - executor, accessibilityManagerWrapper, uiEventLogger); + executor, accessibilityManagerWrapper, uiEventLogger, avalancheController); mTouchAcceptanceDelay = BaseHeadsUpManagerTest.TEST_TOUCH_ACCEPTANCE_TIME; mMinimumDisplayTime = BaseHeadsUpManagerTest.TEST_MINIMUM_DISPLAY_TIME; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt index 0ef3d200d1fa..a90d4b2b6061 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt @@ -118,6 +118,12 @@ class DragToInteractView(context: Context) : FrameLayout(context) { iconResId = R.drawable.pip_ic_close_white ) ) + + // Ensure this is unfocusable & uninteractable + isClickable = false + isFocusable = false + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO + // END DragToInteractView modification } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index d3e85e092b3a..1f0459978c3c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -113,13 +113,8 @@ class MenuAnimationController { /* Moves position without updating underlying percentage position. Can be animated. */ void moveToPosition(PointF position, boolean animateMovement) { - if (Flags.floatingMenuImeDisplacementAnimation()) { - moveToPositionX(position.x, animateMovement); - moveToPositionY(position.y, animateMovement); - } else { - moveToPositionX(position.x, /* animateMovement = */ false); - moveToPositionY(position.y, /* animateMovement = */ false); - } + moveToPositionX(position.x, animateMovement); + moveToPositionY(position.y, animateMovement); } void moveToPositionX(float positionX) { @@ -127,7 +122,7 @@ class MenuAnimationController { } void moveToPositionX(float positionX, boolean animateMovement) { - if (animateMovement && Flags.floatingMenuImeDisplacementAnimation()) { + if (animateMovement) { springMenuWith(DynamicAnimation.TRANSLATION_X, createSpringForce(), /* velocity = */ 0, @@ -142,7 +137,7 @@ class MenuAnimationController { } void moveToPositionY(float positionY, boolean animateMovement) { - if (animateMovement && Flags.floatingMenuImeDisplacementAnimation()) { + if (animateMovement) { springMenuWith(DynamicAnimation.TRANSLATION_Y, createSpringForce(), /* velocity = */ 0, @@ -455,7 +450,7 @@ class MenuAnimationController { ? MIN_PERCENT : Math.min(MAX_PERCENT, position.y / draggableBounds.height()); - if (Flags.floatingMenuImeDisplacementAnimation() && !writeToPosition) { + if (!writeToPosition) { mMenuView.onEdgeChangedIfNeeded(); } else { mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY)); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java index e57323b81490..35fe6b14ee28 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java @@ -74,6 +74,12 @@ class MenuMessageView extends LinearLayout implements addView(mTextView, Index.TEXT_VIEW, new LayoutParams(/* width= */ 0, WRAP_CONTENT, /* weight= */ 1)); addView(mUndoButton, Index.UNDO_BUTTON, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + + // The message box is not focusable, but will announce its contents when it appears. + // The textView and button are still interactable. + setClickable(false); + setFocusable(false); + setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 577bbc0bd840..270fedcf3617 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -101,6 +101,10 @@ class MenuView extends FrameLayout implements loadLayoutResources(); addView(mTargetFeaturesView); + + setClickable(false); + setFocusable(false); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } @Override @@ -224,8 +228,7 @@ class MenuView extends FrameLayout implements } // We can skip animating if FAB is not visible - if (Flags.floatingMenuImeDisplacementAnimation() - && animateMovement && getVisibility() == VISIBLE) { + if (animateMovement && getVisibility() == VISIBLE) { mMenuAnimationController.moveToPosition(position, /* animateMovement = */ true); // onArrivalAtPosition() is called at the end of the animation. } else { @@ -331,7 +334,7 @@ class MenuView extends FrameLayout implements mMoveToTuckedListener.onMoveToTuckedChanged(isMoveToTucked); } - if (Flags.floatingMenuOverlapsNavBarsFlag() && !Flags.floatingMenuAnimatedTuck()) { + if (!Flags.floatingMenuAnimatedTuck()) { if (isMoveToTucked) { final float halfWidth = getMenuWidth() / 2.0f; final boolean isOnLeftSide = mMenuAnimationController.isOnLeftSide(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java index 4865fcedc457..760e1c374e31 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java @@ -34,7 +34,6 @@ import android.view.WindowMetrics; import androidx.annotation.DimenRes; -import com.android.systemui.Flags; import com.android.systemui.res.R; import java.lang.annotation.Retention; @@ -155,11 +154,6 @@ class MenuViewAppearance { final int margin = getMenuMargin(); final Rect draggableBounds = new Rect(getWindowAvailableBounds()); - if (!Flags.floatingMenuOverlapsNavBarsFlag()) { - // Initializes start position for mapping the translation of the menu view. - draggableBounds.offsetTo(/* newLeft= */ 0, /* newTop= */ 0); - } - draggableBounds.top += margin; draggableBounds.right -= getMenuWidth(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index cd3b8a68fb48..97e38f4fc718 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -319,6 +319,9 @@ class MenuViewLayer extends FrameLayout implements if (Flags.floatingMenuAnimatedTuck()) { setClipChildren(true); } + setClickable(false); + setFocusable(false); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } @Override @@ -443,21 +446,18 @@ class MenuViewLayer extends FrameLayout implements } public void onMoveToTuckedChanged(boolean moveToTuck) { - if (Flags.floatingMenuOverlapsNavBarsFlag()) { - if (moveToTuck) { - final Rect bounds = mMenuViewAppearance.getWindowAvailableBounds(); - final int[] location = getLocationOnScreen(); - bounds.offset( - location[0], - location[1] - ); - - setClipBounds(bounds); - } - // Instead of clearing clip bounds when moveToTuck is false, - // wait until the spring animation finishes. + if (moveToTuck) { + final Rect bounds = mMenuViewAppearance.getWindowAvailableBounds(); + final int[] location = getLocationOnScreen(); + bounds.offset( + location[0], + location[1] + ); + + setClipBounds(bounds); } - // Function is a no-operation if flag is disabled. + // Instead of clearing clip bounds when moveToTuck is false, + // wait until the spring animation finishes. } private void onSpringAnimationsEndAction() { @@ -475,9 +475,7 @@ class MenuViewLayer extends FrameLayout implements setClipBounds(null); } } - if (Flags.floatingMenuImeDisplacementAnimation()) { - mMenuView.onArrivalAtPosition(false); - } + mMenuView.onArrivalAtPosition(false); } void dispatchAccessibilityAction(int id) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index bc9d1ffd259b..6b1240b87b72 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -20,11 +20,9 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_ import android.content.Context; import android.graphics.PixelFormat; -import android.view.WindowInsets; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; -import com.android.systemui.Flags; import com.android.systemui.util.settings.SecureSettings; /** @@ -88,14 +86,9 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; params.windowAnimations = android.R.style.Animation_Translucent; // Insets are configured to allow the menu to display over navigation and system bars. - if (Flags.floatingMenuOverlapsNavBarsFlag()) { - params.setFitInsetsTypes(0); - params.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - } else { - params.setFitInsetsTypes( - WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); - } + params.setFitInsetsTypes(0); + params.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; return params; } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt index 3469cfa210ba..e457601a6d52 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -86,7 +86,12 @@ object PromptIconViewBinder { launch { var width = 0 var height = 0 - viewModel.activeAuthType.collect { activeAuthType -> + combine(promptViewModel.size, viewModel.activeAuthType, ::Pair).collect { + (_, activeAuthType) -> + // Every time after bp shows, [isIconViewLoaded] is set to false in + // [BiometricViewSizeBinder]. Then when biometric prompt view is redrew + // (when size or activeAuthType changes), we need to update + // [isIconViewLoaded] here to keep it correct. when (activeAuthType) { AuthType.Fingerprint, AuthType.Coex -> { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 34b50e47600f..86b0b4455f61 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -218,18 +218,12 @@ constructor( */ val faceMode: Flow<Boolean> = combine(modalities, isConfirmationRequired, fingerprintStartMode) { - modalities: BiometricModalities, - isConfirmationRequired: Boolean, - fingerprintStartMode: FingerprintStartMode -> - if (modalities.hasFaceAndFingerprint) { - if (isConfirmationRequired) { - false - } else { - !fingerprintStartMode.isStarted - } - } else { - false - } + modalities, + isConfirmationRequired, + fingerprintStartMode -> + modalities.hasFaceAndFingerprint && + !isConfirmationRequired && + fingerprintStartMode == FingerprintStartMode.Pending } .distinctUntilChanged() @@ -249,14 +243,11 @@ constructor( * asset to be loaded before determining the prompt size. */ val isIconViewLoaded: Flow<Boolean> = - combine(credentialKind, _isIconViewLoaded.asStateFlow()) { credentialKind, isIconViewLoaded - -> - if (credentialKind is PromptKind.Biometric) { - isIconViewLoaded - } else { - true + combine(modalities, _isIconViewLoaded.asStateFlow()) { modalities, isIconViewLoaded -> + val noIcon = modalities.isEmpty + noIcon || isIconViewLoaded } - } + .distinctUntilChanged() // Sets whether the prompt's iconView animation has been loaded in the view yet. fun setIsIconViewLoaded(iconViewLoaded: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index 3819e614aca0..4f4f3d0324b3 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -97,6 +97,14 @@ class FalsingCollectorImpl implements FalsingCollector { } }; + private final KeyguardStateController.Callback mKeyguardStateControllerCallback = + new KeyguardStateController.Callback() { + @Override + public void onKeyguardShowingChanged() { + updateSensorRegistration(); + } + }; + private final KeyguardUpdateMonitorCallback mKeyguardUpdateCallback = new KeyguardUpdateMonitorCallback() { @@ -176,6 +184,8 @@ class FalsingCollectorImpl implements FalsingCollector { mStatusBarStateController.addCallback(mStatusBarStateListener); mState = mStatusBarStateController.getState(); + mKeyguardStateController.addCallback(mKeyguardStateControllerCallback); + mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); mJavaAdapter.alwaysCollectFlow( @@ -364,6 +374,24 @@ class FalsingCollectorImpl implements FalsingCollector { } else { sessionEnd(); } + updateSensorRegistration(); + } + + private boolean shouldBeRegisteredToSensors() { + return mScreenOn + && (mState == StatusBarState.KEYGUARD + || (mState == StatusBarState.SHADE + && mKeyguardStateController.isOccluded() + && mKeyguardStateController.isShowing())) + && !mShowingAod; + } + + private void updateSensorRegistration() { + if (shouldBeRegisteredToSensors()) { + registerSensors(); + } else { + unregisterSensors(); + } } private void sessionStart() { @@ -371,7 +399,6 @@ class FalsingCollectorImpl implements FalsingCollector { logDebug("Starting Session"); mSessionStarted = true; mFalsingDataProvider.setJustUnlockedWithFace(false); - registerSensors(); mFalsingDataProvider.onSessionStarted(); } } @@ -380,7 +407,6 @@ class FalsingCollectorImpl implements FalsingCollector { if (mSessionStarted) { logDebug("Ending Session"); mSessionStarted = false; - unregisterSensors(); mFalsingDataProvider.onSessionEnd(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 2745817d6d40..5e87a7aa9320 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -350,6 +350,9 @@ constructor( if (shadeExpansion > 0f || qsExpansion > 0f) { if (configurationBasedDimensions.useSplitShade) { emit(1f) + } else if (qsExpansion == 1f) { + // Ensure HUNs will be visible in QS shade (at least while unlocked) + emit(1f) } else { // Fade as QS shade expands emit(1f - qsExpansion) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index a155e94584e3..24be3db6231f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.AnimationStateHandler; +import com.android.systemui.statusbar.policy.AvalancheController; import com.android.systemui.statusbar.policy.BaseHeadsUpManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.HeadsUpManagerLogger; @@ -124,9 +125,10 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger, JavaAdapter javaAdapter, - ShadeInteractor shadeInteractor) { + ShadeInteractor shadeInteractor, + AvalancheController avalancheController) { super(context, logger, handler, globalSettings, systemClock, executor, - accessibilityManagerWrapper, uiEventLogger); + accessibilityManagerWrapper, uiEventLogger, avalancheController); Resources resources = mContext.getResources(); mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time); statusBarStateController.addCallback(mStatusBarStateListener); @@ -279,7 +281,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp if (headsUpEntry != null && headsUpEntry.mRemoteInputActive != remoteInputActive) { headsUpEntry.mRemoteInputActive = remoteInputActive; if (remoteInputActive) { - headsUpEntry.removeAutoRemovalCallbacks("setRemoteInputActive(true)"); + headsUpEntry.cancelAutoRemovalCallbacks("setRemoteInputActive(true)"); } else { headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)"); } @@ -482,7 +484,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp this.mExpanded = expanded; if (expanded) { - removeAutoRemovalCallbacks("setExpanded(true)"); + cancelAutoRemovalCallbacks("setExpanded(true)"); } else { updateEntry(false /* updatePostTime */, "setExpanded(false)"); } @@ -495,7 +497,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp mGutsShownPinned = gutsShownPinned; if (gutsShownPinned) { - removeAutoRemovalCallbacks("setGutsShownPinned(true)"); + cancelAutoRemovalCallbacks("setGutsShownPinned(true)"); } else { updateEntry(false /* updatePostTime */, "setGutsShownPinned(false)"); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt new file mode 100644 index 000000000000..6aaf5d60ac0b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy + +import android.util.Log +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun +import com.android.systemui.statusbar.policy.BaseHeadsUpManager.HeadsUpEntry +import javax.inject.Inject + +/* + * Control when heads up notifications show during an avalanche where notifications arrive in fast + * succession, by delaying visual listener side effects and removal handling from BaseHeadsUpManager + */ +@SysUISingleton +class AvalancheController @Inject constructor() { + + private val tag = "AvalancheController" + private val debug = false + + // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD + @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null + + // List of runnables to run for the HUN showing right now + private var headsUpEntryShowingRunnableList: MutableList<Runnable> = ArrayList() + + // HeadsUpEntry waiting to show + // Use sortable list instead of priority queue for debugging + private val nextList: MutableList<HeadsUpEntry> = ArrayList() + + // Map of HeadsUpEntry waiting to show, and runnables to run when it shows. + // Use HashMap instead of SortedMap for faster lookup, and also because the ordering + // provided by HeadsUpEntry.compareTo is not consistent over time or with HeadsUpEntry.equals + @VisibleForTesting var nextMap: MutableMap<HeadsUpEntry, MutableList<Runnable>> = HashMap() + + // Map of Runnable to label for debugging only + private val debugRunnableLabelMap: MutableMap<Runnable, String> = HashMap() + + // HeadsUpEntry we did not show at all because they are not the top priority hun in their batch + // For debugging only + @VisibleForTesting var debugDropSet: MutableSet<HeadsUpEntry> = HashSet() + + /** + * Run or delay Runnable for given HeadsUpEntry + */ + fun update(entry: HeadsUpEntry, runnable: Runnable, label: String) { + if (!NotificationThrottleHun.isEnabled) { + runnable.run() + return + } + val fn = "[$label] => AvalancheController.update ${getKey(entry)}" + + if (debug) { + debugRunnableLabelMap[runnable] = label + } + + if (isShowing(entry)) { + log {"$fn => [update showing]" } + runnable.run() + } else if (entry in nextMap) { + log { "$fn => [update next]" } + nextMap[entry]?.add(runnable) + } else if (headsUpEntryShowing == null) { + log { "$fn => [showNow]" } + showNow(entry, arrayListOf(runnable)) + } else { + // Clean up invalid state when entry is in list but not map and vice versa + if (entry in nextMap) nextMap.remove(entry) + if (entry in nextList) nextList.remove(entry) + + addToNext(entry, runnable) + + // Shorten headsUpEntryShowing display time + val nextIndex = nextList.indexOf(entry) + val isOnlyNextEntry = nextIndex == 0 && nextList.size == 1 + if (isOnlyNextEntry) { + // HeadsUpEntry.updateEntry recursively calls AvalancheController#update + // and goes to the isShowing case above + headsUpEntryShowing!!.updateEntry(false, "avalanche duration update") + } + } + logState("after $fn") + } + + @VisibleForTesting + fun addToNext(entry: HeadsUpEntry, runnable: Runnable) { + nextMap[entry] = arrayListOf(runnable) + nextList.add(entry) + } + + /** + * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete + * all Runnables associated with that entry. + */ + fun delete(entry: HeadsUpEntry, runnable: Runnable, label: String) { + if (!NotificationThrottleHun.isEnabled) { + runnable.run() + return + } + val fn = "[$label] => AvalancheController.delete " + getKey(entry) + + if (entry in nextMap) { + log { "$fn => [remove from next]" } + if (entry in nextMap) nextMap.remove(entry) + if (entry in nextList) nextList.remove(entry) + } else if (entry in debugDropSet) { + log { "$fn => [remove from dropset]" } + debugDropSet.remove(entry) + } else if (isShowing(entry)) { + log { "$fn => [remove showing ${getKey(entry)}]" } + runnable.run() + showNext() + } else { + log { "$fn => [removing untracked ${getKey(entry)}]" } + } + logState("after $fn") + } + + /** + * Returns true if given HeadsUpEntry is the last one tracked by AvalancheController. Used by + * BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration during active + * avalanche. + */ + fun shortenDuration(entry: HeadsUpEntry): Boolean { + if (!NotificationThrottleHun.isEnabled) { + // Use default display duration, like we always did before AvalancheController existed + return false + } + val showingList: MutableList<HeadsUpEntry> = mutableListOf() + headsUpEntryShowing?.let { showingList.add(it) } + val allEntryList = showingList + nextList + + // Shorten duration if not last entry + return allEntryList.indexOf(entry) != allEntryList.size - 1 + } + + /** + * Return true if entry is waiting to show. + */ + fun isWaiting(key: String): Boolean { + if (!NotificationThrottleHun.isEnabled) { + return false + } + for (entry in nextMap.keys) { + if (entry.mEntry?.key.equals(key)) { + return true + } + } + return false + } + + /** + * Return list of keys for huns waiting + */ + fun getWaitingKeys(): MutableList<String> { + if (!NotificationThrottleHun.isEnabled) { + return mutableListOf() + } + val keyList = mutableListOf<String>() + for (entry in nextMap.keys) { + entry.mEntry?.let { keyList.add(entry.mEntry!!.key) } + } + return keyList + } + + private fun isShowing(entry: HeadsUpEntry): Boolean { + return headsUpEntryShowing != null && entry.mEntry?.key == headsUpEntryShowing?.mEntry?.key + } + + private fun showNow(entry: HeadsUpEntry, runnableList: MutableList<Runnable>) { + log { "show " + getKey(entry) + " backlog size: " + runnableList.size } + + headsUpEntryShowing = entry + + runnableList.forEach { + if (it in debugRunnableLabelMap) { + log { "run runnable from: ${debugRunnableLabelMap[it]}" } + } + it.run() + } + } + + private fun showNext() { + log { "showNext" } + headsUpEntryShowing = null + + if (nextList.isEmpty()) { + log { "no more to show!" } + return + } + + // Only show first (top priority) entry in next batch + nextList.sort() + headsUpEntryShowing = nextList[0] + headsUpEntryShowingRunnableList = nextMap[headsUpEntryShowing]!! + + // Remove runnable labels for dropped huns + val listToDrop = nextList.subList(1, nextList.size) + if (debug) { + // Clear runnable labels + for (e in listToDrop) { + val runnableList = nextMap[e]!! + for (r in runnableList) { + debugRunnableLabelMap.remove(r) + } + } + debugDropSet.addAll(listToDrop) + } + + clearNext() + showNow(headsUpEntryShowing!!, headsUpEntryShowingRunnableList) + } + + fun clearNext() { + nextList.clear() + nextMap.clear() + } + + // Methods below are for logging only ========================================================== + + private inline fun log(s: () -> String) { + if (debug) { + Log.d(tag, s()) + } + } + + // TODO(b/315362456) expose as dumpable for bugreports + private fun logState(reason: String) { + log { "state $reason" } + log { "showing: " + getKey(headsUpEntryShowing) } + log { "next list: $nextListStr map: $nextMapStr" } + log { "drop: $dropSetStr" } + } + + private val dropSetStr: String + get() { + val queue = ArrayList<String>() + for (entry in debugDropSet) { + queue.add(getKey(entry)) + } + return java.lang.String.join(" ", queue) + } + + private val nextListStr: String + get() { + val queue = ArrayList<String>() + for (entry in nextList) { + queue.add(getKey(entry)) + } + return java.lang.String.join(" ", queue) + } + + private val nextMapStr: String + get() { + val queue = ArrayList<String>() + for (entry in nextMap.keys) { + queue.add(getKey(entry)) + } + return java.lang.String.join(" ", queue) + } + + fun getKey(entry: HeadsUpEntry?): String { + if (entry == null) { + return "null" + } + if (entry.mEntry == null) { + return entry.toString() + } + return entry.mEntry!!.key + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 530e49c83802..05cc73edd892 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -45,6 +45,8 @@ import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; /** @@ -68,6 +70,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { private final AccessibilityManagerWrapper mAccessibilityMgr; private final UiEventLogger mUiEventLogger; + private final AvalancheController mAvalancheController; protected final SystemClock mSystemClock; protected final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); @@ -100,13 +103,15 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { SystemClock systemClock, @Main DelayableExecutor executor, AccessibilityManagerWrapper accessibilityManagerWrapper, - UiEventLogger uiEventLogger) { + UiEventLogger uiEventLogger, + AvalancheController avalancheController) { mLogger = logger; mExecutor = executor; mSystemClock = systemClock; mContext = context; mAccessibilityMgr = accessibilityManagerWrapper; mUiEventLogger = uiEventLogger; + mAvalancheController = avalancheController; Resources resources = context.getResources(); mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time); mStickyForSomeTimeAutoDismissTime = resources.getInteger( @@ -157,18 +162,26 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ @Override public void showNotification(@NonNull NotificationEntry entry) { - mLogger.logShowNotification(entry); - - // Add new entry and begin managing it HeadsUpEntry headsUpEntry = createHeadsUpEntry(); + + // Attach NotificationEntry for AvalancheController to log key and + // record mPostTime for AvalancheController sorting headsUpEntry.setEntry(entry); - mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry); - onEntryAdded(headsUpEntry); - entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - entry.setIsHeadsUpEntry(true); - updateNotification(entry.getKey(), true /* shouldHeadsUpAgain */); - entry.setInterruption(); + Runnable runnable = () -> { + // TODO(b/315362456) log outside runnable too + mLogger.logShowNotification(entry); + + // Add new entry and begin managing it + mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry); + onEntryAdded(headsUpEntry); + entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + entry.setIsHeadsUpEntry(true); + + updateNotificationInternal(entry.getKey(), true /* shouldHeadsUpAgain */); + entry.setInterruption(); + }; + mAvalancheController.update(headsUpEntry, runnable, "showNotification"); } /** @@ -181,6 +194,11 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { @Override public boolean removeNotification(@NonNull String key, boolean releaseImmediately) { mLogger.logRemoveNotification(key, releaseImmediately); + + if (mAvalancheController.isWaiting(key)) { + removeEntry(key); + return true; + } HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); if (headsUpEntry == null) { return true; @@ -203,6 +221,14 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) { HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); + Runnable runnable = () -> { + updateNotificationInternal(key, shouldHeadsUpAgain); + }; + mAvalancheController.update(headsUpEntry, runnable, "updateNotification"); + } + + private void updateNotificationInternal(@NonNull String key, boolean shouldHeadsUpAgain) { + HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); mLogger.logUpdateNotification(key, shouldHeadsUpAgain, headsUpEntry != null); if (headsUpEntry == null) { // the entry was released before this update (i.e by a listener) This can happen @@ -231,12 +257,16 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { for (String key : keysToRemove) { removeEntry(key); } + for (String key : mAvalancheController.getWaitingKeys()) { + removeEntry(key); + } } /** * Returns the entry if it is managed by this manager. * @param key key of notification * @return the entry + * TODO(b/315362456) See if caller needs to check AvalancheController waiting entries */ @Nullable public NotificationEntry getEntry(@NonNull String key) { @@ -251,6 +281,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { @NonNull @Override public Stream<NotificationEntry> getAllEntries() { + // TODO(b/315362456) See if callers need to check AvalancheController return mHeadsUpEntryMap.values().stream().map(headsUpEntry -> headsUpEntry.mEntry); } @@ -267,7 +298,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * @return true if the notification is managed by this manager */ public boolean isHeadsUpEntry(@NonNull String key) { - return mHeadsUpEntryMap.containsKey(key); + return mHeadsUpEntryMap.containsKey(key) || mAvalancheController.isWaiting(key); } /** @@ -331,7 +362,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * Manager-specific logic that should occur when an entry is added. * @param headsUpEntry entry added */ - protected void onEntryAdded(HeadsUpEntry headsUpEntry) { + void onEntryAdded(HeadsUpEntry headsUpEntry) { NotificationEntry entry = headsUpEntry.mEntry; entry.setHeadsUp(true); @@ -349,20 +380,24 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ protected final void removeEntry(@NonNull String key) { HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); - if (headsUpEntry == null) { - return; - } - NotificationEntry entry = headsUpEntry.mEntry; - // If the notification is animating, we will remove it at the end of the animation. - if (entry != null && entry.isExpandAnimationRunning()) { - return; - } - entry.demoteStickyHun(); - mHeadsUpEntryMap.remove(key); - onEntryRemoved(headsUpEntry); - entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - headsUpEntry.reset(); + Runnable runnable = () -> { + if (headsUpEntry == null) { + return; + } + NotificationEntry entry = headsUpEntry.mEntry; + + // If the notification is animating, we will remove it at the end of the animation. + if (entry != null && entry.isExpandAnimationRunning()) { + return; + } + entry.demoteStickyHun(); + mHeadsUpEntryMap.remove(key); + onEntryRemoved(headsUpEntry); + entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + headsUpEntry.reset(); + }; + mAvalancheController.delete(headsUpEntry, runnable, "removeEntry"); } /** @@ -380,7 +415,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } } - protected void updatePinnedMode() { + private void updatePinnedMode() { boolean hasPinnedNotification = hasPinnedNotificationInternal(); if (hasPinnedNotification == mHasPinnedNotification) { return; @@ -416,7 +451,9 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * Snoozes all current Heads Up Notifications. */ public void snooze() { - for (String key : mHeadsUpEntryMap.keySet()) { + List<String> keySet = new ArrayList<>(mHeadsUpEntryMap.keySet()); + keySet.addAll(mAvalancheController.getWaitingKeys()); + for (String key : keySet) { HeadsUpEntry entry = getHeadsUpEntry(key); String packageName = entry.mEntry.getSbn().getPackageName(); String snoozeKey = snoozeKey(packageName, mUser); @@ -432,6 +469,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { @Nullable protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) { + // TODO(b/315362456) See if callers need to check AvalancheController return (HeadsUpEntry) mHeadsUpEntryMap.get(key); } @@ -515,18 +553,22 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ public void unpinAll(boolean userUnPinned) { for (String key : mHeadsUpEntryMap.keySet()) { - HeadsUpEntry entry = getHeadsUpEntry(key); - setEntryPinned(entry, false /* isPinned */); - // maybe it got un sticky - entry.updateEntry(false /* updatePostTime */, "unpinAll"); - - // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay - // on the screen. - if (userUnPinned && entry.mEntry != null) { - if (entry.mEntry.mustStayOnScreen()) { - entry.mEntry.setHeadsUpIsVisible(); + HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); + + Runnable runnable = () -> { + setEntryPinned(headsUpEntry, false /* isPinned */); + // maybe it got un sticky + headsUpEntry.updateEntry(false /* updatePostTime */, "unpinAll"); + + // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay + // on the screen. + if (userUnPinned && headsUpEntry.mEntry != null) { + if (headsUpEntry.mEntry.mustStayOnScreen()) { + headsUpEntry.mEntry.setHeadsUpIsVisible(); + } } - } + }; + mAvalancheController.delete(headsUpEntry, runnable, "unpinAll"); } } @@ -606,6 +648,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ @Override public boolean isSticky(String key) { + // TODO(b/315362456) See if callers need to check AvalancheController HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); if (headsUpEntry != null) { return headsUpEntry.isSticky(); @@ -633,9 +676,10 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { /** * This represents a notification and how long it is in a heads up mode. It also manages its - * lifecycle automatically when created. + * lifecycle automatically when created. This class is public because it is exposed by methods + * of AvalancheController that take it as param. */ - protected class HeadsUpEntry implements Comparable<HeadsUpEntry> { + public class HeadsUpEntry implements Comparable<HeadsUpEntry> { public boolean mRemoteInputActive; public boolean mUserActionMayIndirectlyRemove; @@ -672,27 +716,41 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } /** + * An interface that returns the amount of time left this HUN should show. + */ + interface FinishTimeUpdater { + long updateAndGetTimeRemaining(); + } + + /** * Updates an entry's removal time. * @param updatePostTime whether or not to refresh the post time */ public void updateEntry(boolean updatePostTime, @Nullable String reason) { - mLogger.logUpdateEntry(mEntry, updatePostTime, reason); + Runnable runnable = () -> { + mLogger.logUpdateEntry(mEntry, updatePostTime, reason); - final long now = mSystemClock.elapsedRealtime(); - mEarliestRemovalTime = now + mMinimumDisplayTime; + final long now = mSystemClock.elapsedRealtime(); + mEarliestRemovalTime = now + mMinimumDisplayTime; - if (updatePostTime) { - mPostTime = Math.max(mPostTime, now); - } + if (updatePostTime) { + mPostTime = Math.max(mPostTime, now); + } + }; + mAvalancheController.update(this, runnable, "updateEntry (updatePostTime)"); if (isSticky()) { - removeAutoRemovalCallbacks("updateEntry (sticky)"); + cancelAutoRemovalCallbacks("updateEntry (sticky)"); return; } - final long finishTime = calculateFinishTime(); - final long timeLeft = Math.max(finishTime - now, mMinimumDisplayTime); - scheduleAutoRemovalCallback(timeLeft, "updateEntry (not sticky)"); + FinishTimeUpdater finishTimeCalculator = () -> { + final long finishTime = calculateFinishTime(); + final long now = mSystemClock.elapsedRealtime(); + final long timeLeft = Math.max(finishTime - now, mMinimumDisplayTime); + return timeLeft; + }; + scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); } /** @@ -758,12 +816,31 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } } + @Override + public int hashCode() { + if (mEntry == null) return super.hashCode(); + int result = mEntry.getKey().hashCode(); + result = 31 * result; + return result; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || !(o instanceof HeadsUpEntry)) return false; + HeadsUpEntry otherHeadsUpEntry = (HeadsUpEntry) o; + if (mEntry != null && otherHeadsUpEntry.mEntry != null) { + return mEntry.getKey().equals(otherHeadsUpEntry.mEntry.getKey()); + } + return false; + } + public void setExpanded(boolean expanded) { this.mExpanded = expanded; } public void reset() { - removeAutoRemovalCallbacks("reset()"); + cancelAutoRemovalCallbacks("reset()"); mEntry = null; mRemoveRunnable = null; mExpanded = false; @@ -773,37 +850,48 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { /** * Clear any pending removal runnables. */ - public void removeAutoRemovalCallbacks(@Nullable String reason) { - final boolean removed = removeAutoRemovalCallbackInternal(); + public void cancelAutoRemovalCallbacks(@Nullable String reason) { + Runnable runnable = () -> { + final boolean removed = cancelAutoRemovalCallbackInternal(); - if (removed) { - mLogger.logAutoRemoveCanceled(mEntry, reason); - } + if (removed) { + mLogger.logAutoRemoveCanceled(mEntry, reason); + } + }; + mAvalancheController.update(this, runnable, + reason + " removeAutoRemovalCallbacks"); } - public void scheduleAutoRemovalCallback(long delayMillis, @NonNull String reason) { - if (mRemoveRunnable == null) { - Log.wtf(TAG, "scheduleAutoRemovalCallback with no callback set"); - return; - } + public void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, + @NonNull String reason) { - final boolean removed = removeAutoRemovalCallbackInternal(); + Runnable runnable = () -> { + long delayMs = finishTimeCalculator.updateAndGetTimeRemaining(); - if (removed) { - mLogger.logAutoRemoveRescheduled(mEntry, delayMillis, reason); - } else { - mLogger.logAutoRemoveScheduled(mEntry, delayMillis, reason); - } + if (mRemoveRunnable == null) { + Log.wtf(TAG, "scheduleAutoRemovalCallback with no callback set"); + return; + } - mCancelRemoveRunnable = mExecutor.executeDelayed(mRemoveRunnable, - delayMillis); + final boolean deletedExistingRemovalRunnable = cancelAutoRemovalCallbackInternal(); + mCancelRemoveRunnable = mExecutor.executeDelayed(mRemoveRunnable, + delayMs); + + if (deletedExistingRemovalRunnable) { + mLogger.logAutoRemoveRescheduled(mEntry, delayMs, reason); + } else { + mLogger.logAutoRemoveScheduled(mEntry, delayMs, reason); + } + }; + mAvalancheController.update(this, runnable, + reason + " scheduleAutoRemovalCallback"); } - public boolean removeAutoRemovalCallbackInternal() { + public boolean cancelAutoRemovalCallbackInternal() { final boolean scheduled = (mCancelRemoveRunnable != null); if (scheduled) { - mCancelRemoveRunnable.run(); + mCancelRemoveRunnable.run(); // Delete removal runnable from Executor queue mCancelRemoveRunnable = null; } @@ -815,8 +903,12 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ public void removeAsSoonAsPossible() { if (mRemoveRunnable != null) { - final long timeLeft = mEarliestRemovalTime - mSystemClock.elapsedRealtime(); - scheduleAutoRemovalCallback(timeLeft, "removeAsSoonAsPossible"); + + FinishTimeUpdater finishTimeCalculator = () -> { + final long timeLeft = mEarliestRemovalTime - mSystemClock.elapsedRealtime(); + return timeLeft; + }; + scheduleAutoRemovalCallback(finishTimeCalculator, "removeAsSoonAsPossible"); } } @@ -834,9 +926,15 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * {@link SystemClock#elapsedRealtime()} */ protected long calculateFinishTime() { - final long duration = getRecommendedHeadsUpTimeoutMs( - isStickyForSomeTime() ? mStickyForSomeTimeAutoDismissTime : mAutoDismissTime); - + int requestedTimeOutMs; + if (isStickyForSomeTime()) { + requestedTimeOutMs = mStickyForSomeTimeAutoDismissTime; + } else if (mAvalancheController.shortenDuration(this)) { + requestedTimeOutMs = 1000; + } else { + requestedTimeOutMs = mAutoDismissTime; + } + final long duration = getRecommendedHeadsUpTimeoutMs(requestedTimeOutMs); return mPostTime + duration; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index de795a744129..f76957f85009 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -308,20 +308,6 @@ public class MenuViewLayerTest extends SysuiTestCase { } @Test - @DisableFlags(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION) - public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme_old() { - mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 100)); - final PointF beforePosition = mMenuView.getMenuPosition(); - - dispatchShowingImeInsets(); - - final float menuBottom = mMenuView.getTranslationY() + mMenuView.getMenuHeight(); - assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x); - assertThat(menuBottom).isLessThan(beforePosition.y); - } - - @Test - @EnableFlags(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION) public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme() { mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 100)); final PointF beforePosition = mMenuView.getMenuPosition(); @@ -337,19 +323,6 @@ public class MenuViewLayerTest extends SysuiTestCase { } @Test - @DisableFlags(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION) - public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition_old() { - mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 200)); - final PointF beforePosition = mMenuView.getMenuPosition(); - - dispatchHidingImeInsets(); - - assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x); - assertThat(mMenuView.getTranslationY()).isEqualTo(beforePosition.y); - } - - @Test - @EnableFlags(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION) public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition() { mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 200)); final PointF beforePosition = mMenuView.getMenuPosition(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 1aab9e8f800c..7db4ca966890 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -1338,6 +1338,17 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(logoDescription).isEqualTo(logoDescriptionFromApp) } + @Test + fun iconViewLoaded() = runGenericTest { + val isIconViewLoaded by collectLastValue(viewModel.isIconViewLoaded) + // TODO(b/328677869): Add test for noIcon logic. + assertThat(isIconViewLoaded).isFalse() + + viewModel.setIsIconViewLoaded(true) + + assertThat(isIconViewLoaded).isTrue() + } + /** Asserts that the selected buttons are visible now. */ private suspend fun TestScope.assertButtonsVisible( tryAgain: Boolean = false, diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java index 3f13033217b3..45d20dcd9d11 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java @@ -188,6 +188,20 @@ public class FalsingCollectorImplTest extends SysuiTestCase { } @Test + public void testRegisterSensor_OccludingActivity() { + when(mKeyguardStateController.isOccluded()).thenReturn(true); + + ArgumentCaptor<StatusBarStateController.StateListener> stateListenerArgumentCaptor = + ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); + verify(mStatusBarStateController).addCallback(stateListenerArgumentCaptor.capture()); + + mFalsingCollector.onScreenTurningOn(); + reset(mProximitySensor); + stateListenerArgumentCaptor.getValue().onStateChanged(StatusBarState.SHADE); + verify(mProximitySensor).register(any(ThresholdSensor.Listener.class)); + } + + @Test public void testPassThroughGesture() { MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); MotionEvent up = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); diff --git a/ravenwood/README.md b/ravenwood/README.md index 9c4fda7a50a6..8cafb433736f 100644 --- a/ravenwood/README.md +++ b/ravenwood/README.md @@ -1,9 +1,11 @@ # Ravenwood -Ravenwood is an officially-supported lightweight unit testing environment for Android platform code that runs on the host. +Ravenwood is a lightweight unit testing environment for Android platform code that runs on the host. Ravenwood’s focus on Android platform use-cases, improved maintainability, and device consistency distinguishes it from Robolectric, which remains a popular choice for app testing. +> **Note:** Active development of Ravenwood has been paused as of March 2024. Existing Ravenwood tests will continue running, but support has moved to a self-service model. + ## Background Executing tests on a typical Android device has substantial overhead, such as flashing the build, waiting for the boot to complete, and retrying tests that fail due to general flakiness. diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 52988460606c..0012b3d86552 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -204,6 +204,7 @@ import android.os.TransactionTooLargeException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService; import android.service.voice.HotwordDetectionService; import android.service.voice.VisualQueryDetectionService; import android.service.wearable.WearableSensingService; @@ -4518,13 +4519,14 @@ public final class ActiveServices { } // TODO(b/265746493): Special case for HotwordDetectionService, - // VisualQueryDetectionService and WearableSensingService. + // VisualQueryDetectionService, WearableSensingService and OnDeviceSandboxedInferenceService // Need a cleaner way to append this seInfo. private String generateAdditionalSeInfoFromService(Intent service) { if (service != null && service.getAction() != null && (service.getAction().equals(HotwordDetectionService.SERVICE_INTERFACE) || service.getAction().equals(VisualQueryDetectionService.SERVICE_INTERFACE) - || service.getAction().equals(WearableSensingService.SERVICE_INTERFACE))) { + || service.getAction().equals(WearableSensingService.SERVICE_INTERFACE) + || service.getAction().equals(OnDeviceSandboxedInferenceService.SERVICE_INTERFACE))) { return ":isolatedComputeApp"; } return ""; diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java index c7b60da2fc51..dd6433d98553 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java @@ -19,11 +19,13 @@ package com.android.server.inputmethod; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.content.Context; import android.content.pm.UserInfo; import android.os.Handler; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.DirectBootAwareness; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; @@ -67,7 +69,7 @@ final class AdditionalSubtypeMapRepository { AdditionalSubtypeUtils.save(map, inputMethodMap, userId); } - static void initialize(@NonNull Handler handler) { + static void initialize(@NonNull Handler handler, @NonNull Context context) { final UserManagerInternal userManagerInternal = LocalServices.getService(UserManagerInternal.class); handler.post(() -> { @@ -79,8 +81,16 @@ final class AdditionalSubtypeMapRepository { handler.post(() -> { synchronized (ImfLock.class) { if (!sPerUserMap.contains(userId)) { - sPerUserMap.put(userId, - AdditionalSubtypeUtils.load(userId)); + final AdditionalSubtypeMap additionalSubtypeMap = + AdditionalSubtypeUtils.load(userId); + sPerUserMap.put(userId, additionalSubtypeMap); + final InputMethodSettings settings = + InputMethodManagerService + .queryInputMethodServicesInternal(context, + userId, + additionalSubtypeMap, + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, settings); } } }); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index fef56610b406..a0e910dd7e9c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -284,6 +284,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final Context mContext; final Resources mRes; private final Handler mHandler; + + /** + * TODO(b/329163064): Remove this field. + */ @NonNull @MultiUserUnawareField private InputMethodSettings mSettings; @@ -867,7 +871,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!mSystemReady) { return; } - buildInputMethodListLocked(true); + for (int userId : mUserManagerInternal.getUserIds()) { + final InputMethodSettings settings = queryInputMethodServicesInternal( + mContext, + userId, + AdditionalSubtypeMapRepository.get(userId), + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, settings); + if (userId == mSettings.getUserId()) { + mSettings = settings; + } + } + postInputMethodSettingUpdatedLocked(true /* resetDefaultEnabledIme */); // If the locale is changed, needs to reset the default ime resetDefaultImeLocked(mContext); updateFromSettingsLocked(true); @@ -1063,13 +1078,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final boolean isCurrentUser = (userId == mSettings.getUserId()); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); - final InputMethodSettings settings; - if (isCurrentUser) { - settings = mSettings; - } else { - settings = queryInputMethodServicesInternal(mContext, userId, - additionalSubtypeMap, DirectBootAwareness.AUTO); - } + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); InputMethodInfo curIm = null; String curInputMethodId = settings.getSelectedInputMethod(); @@ -1113,13 +1122,20 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap, settings.getMethodMap()); } - - if (!isCurrentUser - || !(additionalSubtypeChanged || shouldRebuildInputMethodListLocked())) { + if (isCurrentUser + && !(additionalSubtypeChanged || shouldRebuildInputMethodListLocked())) { return; } - buildInputMethodListLocked(false /* resetDefaultEnabledIme */); + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + userId, newAdditionalSubtypeMap, DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, newSettings); + if (!isCurrentUser) { + return; + } + mSettings = queryInputMethodServicesInternal(mContext, userId, + newAdditionalSubtypeMap, DirectBootAwareness.AUTO); + postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); boolean changed = false; @@ -1271,17 +1287,20 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub void onUnlockUser(@UserIdInt int userId) { synchronized (ImfLock.class) { - final int currentUserId = mSettings.getUserId(); if (DEBUG) { - Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + currentUserId); + Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + + mSettings.getUserId()); } - if (userId != currentUserId) { + if (!mSystemReady) { return; } - mSettings = InputMethodSettings.createEmptyMap(userId); - if (mSystemReady) { + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, newSettings); + if (mSettings.getUserId() == userId) { + mSettings = newSettings; // We need to rebuild IMEs. - buildInputMethodListLocked(false /* resetDefaultEnabledIme */); + postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); updateInputMethodsFromSettingsLocked(true /* enabledChanged */); } } @@ -1347,12 +1366,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mShowOngoingImeSwitcherForPhones = false; - AdditionalSubtypeMapRepository.initialize(mHandler); + // InputMethodSettingsRepository should be initialized before buildInputMethodListLocked + InputMethodSettingsRepository.initialize(mHandler, mContext); + AdditionalSubtypeMapRepository.initialize(mHandler, mContext); final int userId = mActivityManagerInternal.getCurrentUserId(); - // mSettings should be created before buildInputMethodListLocked - mSettings = InputMethodSettings.createEmptyMap(userId); + mSettings = InputMethodSettingsRepository.get(userId); mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(context, @@ -1515,7 +1535,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // The mSystemReady flag is set during boot phase, // and user switch would not happen at that time. resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_USER); - buildInputMethodListLocked(initialUserSwitch); + + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + newUserId, AdditionalSubtypeMapRepository.get(newUserId), DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(newUserId, newSettings); + mSettings = newSettings; + postInputMethodSettingUpdatedLocked(initialUserSwitch /* resetDefaultEnabledIme */); if (TextUtils.isEmpty(mSettings.getSelectedInputMethod())) { // This is the first time of the user switch and // set the current ime to the proper one. @@ -1596,7 +1621,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final String defaultImiId = mSettings.getSelectedInputMethod(); final boolean imeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId); - buildInputMethodListLocked(!imeSelectedOnBoot /* resetDefaultEnabledIme */); + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + currentUserId, AdditionalSubtypeMapRepository.get(currentUserId), + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(currentUserId, newSettings); + mSettings = newSettings; + postInputMethodSettingUpdatedLocked( + !imeSelectedOnBoot /* resetDefaultEnabledIme */); updateFromSettingsLocked(true); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), @@ -1703,9 +1734,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && (!connectionless || mBindingController.supportsConnectionlessStylusHandwriting()); } - //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. - //TODO(b/210039666): use cache. - final InputMethodSettings settings = queryMethodMapForUserLocked(userId); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final InputMethodInfo imi = settings.getMethodMap().get( settings.getSelectedInputMethod()); return imi != null && imi.supportsStylusHandwriting() @@ -1729,9 +1758,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness, int callingUid) { final InputMethodSettings settings; - if (userId == mSettings.getUserId() - && directBootAwareness == DirectBootAwareness.AUTO) { - settings = mSettings; + if (directBootAwareness == DirectBootAwareness.AUTO) { + settings = InputMethodSettingsRepository.get(userId); } else { final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); @@ -1755,7 +1783,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub methodList = mSettings.getEnabledInputMethodList(); settings = mSettings; } else { - settings = queryMethodMapForUserLocked(userId); + settings = InputMethodSettingsRepository.get(userId); methodList = settings.getEnabledInputMethodList(); } // filter caller's access to input methods @@ -1815,22 +1843,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId, int callingUid) { - if (userId == mSettings.getUserId()) { - final InputMethodInfo imi; - String selectedMethodId = getSelectedMethodIdLocked(); - if (imiId == null && selectedMethodId != null) { - imi = mSettings.getMethodMap().get(selectedMethodId); - } else { - imi = mSettings.getMethodMap().get(imiId); - } - if (imi == null || !canCallerAccessInputMethod( - imi.getPackageName(), callingUid, userId, mSettings)) { - return Collections.emptyList(); - } - return mSettings.getEnabledInputMethodSubtypeList( - imi, allowsImplicitlyEnabledSubtypes); - } - final InputMethodSettings settings = queryMethodMapForUserLocked(userId); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final InputMethodInfo imi = settings.getMethodMap().get(imiId); if (imi == null) { return Collections.emptyList(); @@ -3996,8 +4009,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return mSettings.getLastInputMethodSubtype(); } - final InputMethodSettings settings = queryMethodMapForUserLocked(userId); - return settings.getLastInputMethodSubtype(); + return InputMethodSettingsRepository.get(userId).getLastInputMethodSubtype(); } } @@ -4029,19 +4041,21 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); final boolean isCurrentUser = (mSettings.getUserId() == userId); - final InputMethodSettings settings = isCurrentUser - ? mSettings - : queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - DirectBootAwareness.AUTO); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var newAdditionalSubtypeMap = settings.getNewAdditionalSubtypeMap( imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); if (additionalSubtypeMap != newAdditionalSubtypeMap) { AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap, settings.getMethodMap()); + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + userId, AdditionalSubtypeMapRepository.get(userId), + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, newSettings); if (isCurrentUser) { final long ident = Binder.clearCallingIdentity(); try { - buildInputMethodListLocked(false /* resetDefaultEnabledIme */); + mSettings = newSettings; + postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); } finally { Binder.restoreCallingIdentity(ident); } @@ -4071,8 +4085,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub try { synchronized (ImfLock.class) { final boolean currentUser = (mSettings.getUserId() == userId); - final InputMethodSettings settings = currentUser - ? mSettings : queryMethodMapForUserLocked(userId); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) { return; } @@ -4969,7 +4982,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") - void buildInputMethodListLocked(boolean resetDefaultEnabledIme) { + void postInputMethodSettingUpdatedLocked(boolean resetDefaultEnabledIme) { if (DEBUG) { Slog.d(TAG, "--- re-buildInputMethodList reset = " + resetDefaultEnabledIme + " \n ------ caller=" + Debug.getCallers(10)); @@ -4981,10 +4994,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mMethodMapUpdateCount++; mMyPackageMonitor.clearKnownImePackageNamesLocked(); - mSettings = queryInputMethodServicesInternal(mContext, mSettings.getUserId(), - AdditionalSubtypeMapRepository.get(mSettings.getUserId()), - DirectBootAwareness.AUTO); - // Construct the set of possible IME packages for onPackageChanged() to avoid false // negatives when the package state remains to be the same but only the component state is // changed. @@ -5255,8 +5264,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return getCurrentInputMethodSubtypeLocked(); } - final InputMethodSettings settings = queryMethodMapForUserLocked(userId); - return settings.getCurrentInputMethodSubtypeForNonCurrentUsers(); + return InputMethodSettingsRepository.get(userId) + .getCurrentInputMethodSubtypeForNonCurrentUsers(); } } @@ -5318,27 +5327,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) { - final InputMethodSettings settings; - if (userId == mSettings.getUserId()) { - settings = mSettings; - } else { - final AdditionalSubtypeMap additionalSubtypeMap = - AdditionalSubtypeMapRepository.get(userId); - settings = queryInputMethodServicesInternal(mContext, userId, - additionalSubtypeMap, DirectBootAwareness.AUTO); - } + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); return settings.getMethodMap().get(settings.getSelectedInputMethod()); } @GuardedBy("ImfLock.class") - private InputMethodSettings queryMethodMapForUserLocked(@UserIdInt int userId) { - final AdditionalSubtypeMap additionalSubtypeMap = - AdditionalSubtypeMapRepository.get(userId); - return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - DirectBootAwareness.AUTO); - } - - @GuardedBy("ImfLock.class") private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) { if (userId == mSettings.getUserId()) { if (!mSettings.getMethodMap().containsKey(imeId) @@ -5349,7 +5342,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID); return true; } - final InputMethodSettings settings = queryMethodMapForUserLocked(userId); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); if (!settings.getMethodMap().containsKey(imeId) || !settings.getEnabledInputMethodList().contains( settings.getMethodMap().get(imeId))) { @@ -5489,7 +5482,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub setInputMethodEnabledLocked(imeId, enabled); return true; } - final InputMethodSettings settings = queryMethodMapForUserLocked(userId); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); if (!settings.getMethodMap().containsKey(imeId)) { return false; // IME is not found. } @@ -6243,7 +6236,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled); } } else { - final InputMethodSettings settings = queryMethodMapForUserLocked(userId); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); if (enabled) { if (!settings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; @@ -6377,10 +6370,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub nextIme = mSettings.getSelectedInputMethod(); nextEnabledImes = mSettings.getEnabledInputMethodList(); } else { - final AdditionalSubtypeMap additionalSubtypeMap = - AdditionalSubtypeMapRepository.get(userId); - final InputMethodSettings settings = queryInputMethodServicesInternal( - mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); + final InputMethodSettings settings = + InputMethodSettingsRepository.get(userId); nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext, settings.getMethodList()); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java new file mode 100644 index 000000000000..60b9a4cfe840 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.Handler; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.DirectBootAwareness; +import com.android.server.LocalServices; +import com.android.server.pm.UserManagerInternal; + +final class InputMethodSettingsRepository { + @GuardedBy("ImfLock.class") + @NonNull + private static final SparseArray<InputMethodSettings> sPerUserMap = new SparseArray<>(); + + /** + * Not intended to be instantiated. + */ + private InputMethodSettingsRepository() { + } + + @NonNull + @GuardedBy("ImfLock.class") + static InputMethodSettings get(@UserIdInt int userId) { + final InputMethodSettings obj = sPerUserMap.get(userId); + if (obj != null) { + return obj; + } + return InputMethodSettings.createEmptyMap(userId); + } + + @GuardedBy("ImfLock.class") + static void put(@UserIdInt int userId, @NonNull InputMethodSettings obj) { + sPerUserMap.put(userId, obj); + } + + static void initialize(@NonNull Handler handler, @NonNull Context context) { + final UserManagerInternal userManagerInternal = + LocalServices.getService(UserManagerInternal.class); + handler.post(() -> { + userManagerInternal.addUserLifecycleListener( + new UserManagerInternal.UserLifecycleListener() { + @Override + public void onUserRemoved(UserInfo user) { + final int userId = user.id; + handler.post(() -> { + synchronized (ImfLock.class) { + sPerUserMap.remove(userId); + } + }); + } + }); + synchronized (ImfLock.class) { + for (int userId : userManagerInternal.getUserIds()) { + final InputMethodSettings settings = + InputMethodManagerService.queryInputMethodServicesInternal( + context, + userId, + AdditionalSubtypeMapRepository.get(userId), + DirectBootAwareness.AUTO); + sPerUserMap.put(userId, settings); + } + } + }); + } +} diff --git a/core/java/android/app/ondeviceintelligence/Content.aidl b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerInternal.java index 40f0ef9a8541..81f11b52dcb7 100644 --- a/core/java/android/app/ondeviceintelligence/Content.aidl +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerInternal.java @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2024, The Android Open Source Project +/* + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,8 @@ * limitations under the License. */ -package android.app.ondeviceintelligence; +package com.android.server.ondeviceintelligence; -/** - * @hide - */ -parcelable Content; +public interface OnDeviceIntelligenceManagerInternal { + String getRemoteServicePackageName(); +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index 71800efae292..28682e3d916f 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -16,12 +16,11 @@ package com.android.server.ondeviceintelligence; -import static android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceUpdateProcessingException.PROCESSING_UPDATE_STATUS_CONNECTION_FAILED; - import android.Manifest; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.app.AppGlobals; -import android.app.ondeviceintelligence.Content; import android.app.ondeviceintelligence.DownloadCallback; import android.app.ondeviceintelligence.Feature; import android.app.ondeviceintelligence.IDownloadCallback; @@ -33,25 +32,32 @@ import android.app.ondeviceintelligence.IProcessingSignal; import android.app.ondeviceintelligence.IResponseCallback; import android.app.ondeviceintelligence.IStreamingResponseCallback; import android.app.ondeviceintelligence.ITokenInfoCallback; -import android.app.ondeviceintelligence.OnDeviceIntelligenceManager; +import android.app.ondeviceintelligence.OnDeviceIntelligenceException; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.os.Binder; +import android.content.res.Resources; import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; import android.os.ICancellationSignal; +import android.os.Looper; +import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; import android.os.UserHandle; import android.provider.DeviceConfig; import android.service.ondeviceintelligence.IOnDeviceIntelligenceService; import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService; +import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; import android.service.ondeviceintelligence.IRemoteProcessingService; import android.service.ondeviceintelligence.IRemoteStorageService; -import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; import android.service.ondeviceintelligence.OnDeviceIntelligenceService; import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService; import android.text.TextUtils; @@ -62,8 +68,10 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.ServiceConnector; import com.android.internal.os.BackgroundThread; +import com.android.server.LocalServices; import com.android.server.SystemService; +import java.io.FileDescriptor; import java.util.Objects; import java.util.Set; @@ -84,6 +92,9 @@ public class OnDeviceIntelligenceManagerService extends SystemService { private static final String TAG = OnDeviceIntelligenceManagerService.class.getSimpleName(); private static final String KEY_SERVICE_ENABLED = "service_enabled"; + /** Handler message to {@link #resetTemporaryServices()} */ + private static final int MSG_RESET_TEMPORARY_SERVICE = 0; + /** Default value in absence of {@link DeviceConfig} override. */ private static final boolean DEFAULT_SERVICE_ENABLED = true; private static final String NAMESPACE_ON_DEVICE_INTELLIGENCE = "ondeviceintelligence"; @@ -96,19 +107,30 @@ public class OnDeviceIntelligenceManagerService extends SystemService { private RemoteOnDeviceIntelligenceService mRemoteOnDeviceIntelligenceService; volatile boolean mIsServiceEnabled; + @GuardedBy("mLock") + private String[] mTemporaryServiceNames; + + /** + * Handler used to reset the temporary service names. + */ + @GuardedBy("mLock") + private Handler mTemporaryHandler; + public OnDeviceIntelligenceManagerService(Context context) { super(context); mContext = context; + mTemporaryServiceNames = new String[0]; } @Override public void onStart() { publishBinderService( - Context.ON_DEVICE_INTELLIGENCE_SERVICE, new OnDeviceIntelligenceManagerInternal(), + Context.ON_DEVICE_INTELLIGENCE_SERVICE, getOnDeviceIntelligenceManagerService(), /* allowIsolated = */true); + LocalServices.addService(OnDeviceIntelligenceManagerInternal.class, + OnDeviceIntelligenceManagerService.this::getRemoteConfiguredPackageName); } - @Override public void onBootPhase(int phase) { if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { @@ -133,195 +155,211 @@ public class OnDeviceIntelligenceManagerService extends SystemService { KEY_SERVICE_ENABLED, DEFAULT_SERVICE_ENABLED); } - private final class OnDeviceIntelligenceManagerInternal extends - IOnDeviceIntelligenceManager.Stub { - @Override - public void getVersion(RemoteCallback remoteCallback) throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion"); - Objects.requireNonNull(remoteCallback); - mContext.enforceCallingOrSelfPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); - if (!mIsServiceEnabled) { - Slog.w(TAG, "Service not available"); - remoteCallback.sendResult(null); - return; + private IBinder getOnDeviceIntelligenceManagerService() { + return new IOnDeviceIntelligenceManager.Stub() { + @Override + public String getRemoteServicePackageName() { + return OnDeviceIntelligenceManagerService.this.getRemoteConfiguredPackageName(); } - ensureRemoteIntelligenceServiceInitialized(); - mRemoteOnDeviceIntelligenceService.post( - service -> service.getVersion(remoteCallback)); - } - @Override - public void getFeature(int id, IFeatureCallback featureCallback) throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatures"); - Objects.requireNonNull(featureCallback); - mContext.enforceCallingOrSelfPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); - if (!mIsServiceEnabled) { - Slog.w(TAG, "Service not available"); - featureCallback.onFailure( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, - "OnDeviceIntelligenceManagerService is unavailable", - new PersistableBundle()); - return; + @Override + public void getVersion(RemoteCallback remoteCallback) throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion"); + Objects.requireNonNull(remoteCallback); + mContext.enforceCallingOrSelfPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + remoteCallback.sendResult(null); + return; + } + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.run( + service -> service.getVersion(remoteCallback)); } - ensureRemoteIntelligenceServiceInitialized(); - mRemoteOnDeviceIntelligenceService.post( - service -> service.getFeature(Binder.getCallingUid(), id, featureCallback)); - } - @Override - public void listFeatures(IListFeaturesCallback listFeaturesCallback) - throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatures"); - Objects.requireNonNull(listFeaturesCallback); - mContext.enforceCallingOrSelfPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); - if (!mIsServiceEnabled) { - Slog.w(TAG, "Service not available"); - listFeaturesCallback.onFailure( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, - "OnDeviceIntelligenceManagerService is unavailable", - new PersistableBundle()); - return; + @Override + public void getFeature(int id, IFeatureCallback featureCallback) + throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatures"); + Objects.requireNonNull(featureCallback); + mContext.enforceCallingOrSelfPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + featureCallback.onFailure( + OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + return; + } + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.run( + service -> service.getFeature(Binder.getCallingUid(), id, featureCallback)); } - ensureRemoteIntelligenceServiceInitialized(); - mRemoteOnDeviceIntelligenceService.post( - service -> service.listFeatures(Binder.getCallingUid(), listFeaturesCallback)); - } - @Override - public void getFeatureDetails(Feature feature, - IFeatureDetailsCallback featureDetailsCallback) - throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatureStatus"); - Objects.requireNonNull(feature); - Objects.requireNonNull(featureDetailsCallback); - mContext.enforceCallingOrSelfPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); - if (!mIsServiceEnabled) { - Slog.w(TAG, "Service not available"); - featureDetailsCallback.onFailure( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, - "OnDeviceIntelligenceManagerService is unavailable", - new PersistableBundle()); - return; + @Override + public void listFeatures(IListFeaturesCallback listFeaturesCallback) + throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatures"); + Objects.requireNonNull(listFeaturesCallback); + mContext.enforceCallingOrSelfPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + listFeaturesCallback.onFailure( + OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + return; + } + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.run( + service -> service.listFeatures(Binder.getCallingUid(), + listFeaturesCallback)); } - ensureRemoteIntelligenceServiceInitialized(); - mRemoteOnDeviceIntelligenceService.post( - service -> service.getFeatureDetails(Binder.getCallingUid(), feature, - featureDetailsCallback)); - } - @Override - public void requestFeatureDownload(Feature feature, ICancellationSignal cancellationSignal, - IDownloadCallback downloadCallback) throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload"); - Objects.requireNonNull(feature); - Objects.requireNonNull(downloadCallback); - mContext.enforceCallingOrSelfPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); - if (!mIsServiceEnabled) { - Slog.w(TAG, "Service not available"); - downloadCallback.onDownloadFailed( - DownloadCallback.DOWNLOAD_FAILURE_STATUS_UNAVAILABLE, - "OnDeviceIntelligenceManagerService is unavailable", - new PersistableBundle()); + @Override + public void getFeatureDetails(Feature feature, + IFeatureDetailsCallback featureDetailsCallback) + throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatureStatus"); + Objects.requireNonNull(feature); + Objects.requireNonNull(featureDetailsCallback); + mContext.enforceCallingOrSelfPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + featureDetailsCallback.onFailure( + OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + return; + } + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.run( + service -> service.getFeatureDetails(Binder.getCallingUid(), feature, + featureDetailsCallback)); + } + + @Override + public void requestFeatureDownload(Feature feature, + ICancellationSignal cancellationSignal, + IDownloadCallback downloadCallback) throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload"); + Objects.requireNonNull(feature); + Objects.requireNonNull(downloadCallback); + mContext.enforceCallingOrSelfPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + downloadCallback.onDownloadFailed( + DownloadCallback.DOWNLOAD_FAILURE_STATUS_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + } + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.run( + service -> service.requestFeatureDownload(Binder.getCallingUid(), feature, + cancellationSignal, + downloadCallback)); } - ensureRemoteIntelligenceServiceInitialized(); - mRemoteOnDeviceIntelligenceService.post( - service -> service.requestFeatureDownload(Binder.getCallingUid(), feature, - cancellationSignal, - downloadCallback)); - } - @Override - public void requestTokenInfo(Feature feature, - Content request, ICancellationSignal cancellationSignal, - ITokenInfoCallback tokenInfoCallback) throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing"); - Objects.requireNonNull(feature); - Objects.requireNonNull(request); - Objects.requireNonNull(tokenInfoCallback); - - mContext.enforceCallingOrSelfPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); - if (!mIsServiceEnabled) { - Slog.w(TAG, "Service not available"); - tokenInfoCallback.onFailure( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, - "OnDeviceIntelligenceManagerService is unavailable", - new PersistableBundle()); + @Override + public void requestTokenInfo(Feature feature, + Bundle request, ICancellationSignal cancellationSignal, + ITokenInfoCallback tokenInfoCallback) throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing"); + Objects.requireNonNull(feature); + Objects.requireNonNull(request); + Objects.requireNonNull(tokenInfoCallback); + + mContext.enforceCallingOrSelfPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + tokenInfoCallback.onFailure( + OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + } + ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( + service -> service.requestTokenInfo(Binder.getCallingUid(), feature, + request, + cancellationSignal, + tokenInfoCallback)); } - ensureRemoteInferenceServiceInitialized(); - mRemoteInferenceService.post( - service -> service.requestTokenInfo(Binder.getCallingUid(), feature, request, - cancellationSignal, - tokenInfoCallback)); - } - @Override - public void processRequest(Feature feature, - Content request, - int requestType, - ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, - IResponseCallback responseCallback) - throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest"); - Objects.requireNonNull(feature); - Objects.requireNonNull(responseCallback); - mContext.enforceCallingOrSelfPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); - if (!mIsServiceEnabled) { - Slog.w(TAG, "Service not available"); - responseCallback.onFailure( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException.PROCESSING_ERROR_SERVICE_UNAVAILABLE, - "OnDeviceIntelligenceManagerService is unavailable", - new PersistableBundle()); + @Override + public void processRequest(Feature feature, + Bundle request, + int requestType, + ICancellationSignal cancellationSignal, + IProcessingSignal processingSignal, + IResponseCallback responseCallback) + throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest"); + Objects.requireNonNull(feature); + Objects.requireNonNull(responseCallback); + mContext.enforceCallingOrSelfPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + responseCallback.onFailure( + OnDeviceIntelligenceException.PROCESSING_ERROR_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + } + ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( + service -> service.processRequest(Binder.getCallingUid(), feature, request, + requestType, + cancellationSignal, processingSignal, + responseCallback)); } - ensureRemoteInferenceServiceInitialized(); - mRemoteInferenceService.post( - service -> service.processRequest(Binder.getCallingUid(), feature, request, - requestType, - cancellationSignal, processingSignal, - responseCallback)); - } - @Override - public void processRequestStreaming(Feature feature, - Content request, - int requestType, - ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, - IStreamingResponseCallback streamingCallback) throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming"); - Objects.requireNonNull(feature); - Objects.requireNonNull(streamingCallback); - mContext.enforceCallingOrSelfPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); - if (!mIsServiceEnabled) { - Slog.w(TAG, "Service not available"); - streamingCallback.onFailure( - OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException.PROCESSING_ERROR_SERVICE_UNAVAILABLE, - "OnDeviceIntelligenceManagerService is unavailable", - new PersistableBundle()); + @Override + public void processRequestStreaming(Feature feature, + Bundle request, + int requestType, + ICancellationSignal cancellationSignal, + IProcessingSignal processingSignal, + IStreamingResponseCallback streamingCallback) throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming"); + Objects.requireNonNull(feature); + Objects.requireNonNull(streamingCallback); + mContext.enforceCallingOrSelfPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + streamingCallback.onFailure( + OnDeviceIntelligenceException.PROCESSING_ERROR_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + } + ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( + service -> service.processRequestStreaming(Binder.getCallingUid(), feature, + request, requestType, + cancellationSignal, processingSignal, + streamingCallback)); } - ensureRemoteInferenceServiceInitialized(); - mRemoteInferenceService.post( - service -> service.processRequestStreaming(Binder.getCallingUid(), feature, - request, requestType, - cancellationSignal, processingSignal, - streamingCallback)); - } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { + new OnDeviceIntelligenceShellCommand(OnDeviceIntelligenceManagerService.this).exec( + this, in, out, err, args, callback, resultReceiver); + } + }; } private void ensureRemoteIntelligenceServiceInitialized() throws RemoteException { synchronized (mLock) { if (mRemoteOnDeviceIntelligenceService == null) { - String serviceName = mContext.getResources().getString( - R.string.config_defaultOnDeviceIntelligenceService); + String serviceName = getServiceNames()[0]; validateService(serviceName, false); mRemoteOnDeviceIntelligenceService = new RemoteOnDeviceIntelligenceService(mContext, ComponentName.unflattenFromString(serviceName), @@ -352,13 +390,13 @@ public class OnDeviceIntelligenceManagerService extends SystemService { IProcessingUpdateStatusCallback callback) { try { ensureRemoteInferenceServiceInitialized(); - mRemoteInferenceService.post( + mRemoteInferenceService.run( service -> service.updateProcessingState( processingState, callback)); } catch (RemoteException unused) { try { callback.onFailure( - PROCESSING_UPDATE_STATUS_CONNECTION_FAILED, + 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); @@ -371,8 +409,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { private void ensureRemoteInferenceServiceInitialized() throws RemoteException { synchronized (mLock) { if (mRemoteInferenceService == null) { - String serviceName = mContext.getResources().getString( - R.string.config_defaultOnDeviceSandboxedInferenceService); + String serviceName = getServiceNames()[1]; validateService(serviceName, true); mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext, ComponentName.unflattenFromString(serviceName), @@ -384,7 +421,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { @NonNull IOnDeviceSandboxedInferenceService service) { try { ensureRemoteIntelligenceServiceInitialized(); - mRemoteOnDeviceIntelligenceService.post( + mRemoteOnDeviceIntelligenceService.run( intelligenceService -> intelligenceService.notifyInferenceServiceConnected()); service.registerRemoteStorageService( getIRemoteStorageService()); @@ -404,7 +441,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void getReadOnlyFileDescriptor( String filePath, AndroidFuture<ParcelFileDescriptor> future) { - mRemoteOnDeviceIntelligenceService.post( + mRemoteOnDeviceIntelligenceService.run( service -> service.getReadOnlyFileDescriptor( filePath, future)); } @@ -413,7 +450,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void getReadOnlyFeatureFileDescriptorMap( Feature feature, RemoteCallback remoteCallback) { - mRemoteOnDeviceIntelligenceService.post( + mRemoteOnDeviceIntelligenceService.run( service -> service.getReadOnlyFeatureFileDescriptorMap( feature, remoteCallback)); } @@ -469,4 +506,92 @@ public class OnDeviceIntelligenceManagerService extends SystemService { return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0 && (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0; } + + @Nullable + public String getRemoteConfiguredPackageName() { + try { + String[] serviceNames = getServiceNames(); + ComponentName componentName = ComponentName.unflattenFromString(serviceNames[1]); + if (componentName != null) { + return componentName.getPackageName(); + } + } catch (Resources.NotFoundException e) { + Slog.e(TAG, "Could not find resource", e); + } + + return null; + } + + + protected String[] getServiceNames() throws Resources.NotFoundException { + // TODO 329240495 : Consider a small class with explicit field names for the two services + synchronized (mLock) { + if (mTemporaryServiceNames != null && mTemporaryServiceNames.length == 2) { + return mTemporaryServiceNames; + } + } + return new String[]{mContext.getResources().getString( + R.string.config_defaultOnDeviceIntelligenceService), + mContext.getResources().getString( + R.string.config_defaultOnDeviceSandboxedInferenceService)}; + } + + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void setTemporaryServices(@NonNull String[] componentNames, int durationMs) { + Objects.requireNonNull(componentNames); + enforceShellOnly(Binder.getCallingUid(), "setTemporaryServices"); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + synchronized (mLock) { + mTemporaryServiceNames = componentNames; + + if (mTemporaryHandler == null) { + mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_RESET_TEMPORARY_SERVICE) { + synchronized (mLock) { + resetTemporaryServices(); + } + } else { + Slog.wtf(TAG, "invalid handler msg: " + msg); + } + } + }; + } else { + mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); + } + + if (durationMs != -1) { + mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs); + } + } + } + + public void resetTemporaryServices() { + synchronized (mLock) { + if (mTemporaryHandler != null) { + mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); + mTemporaryHandler = null; + } + + mRemoteInferenceService = null; + mRemoteOnDeviceIntelligenceService = null; + mTemporaryServiceNames = new String[0]; + } + } + + /** + * Throws if the caller is not of a shell (or root) UID. + * + * @param callingUid pass Binder.callingUid(). + */ + public static void enforceShellOnly(int callingUid, String message) { + if (callingUid == android.os.Process.SHELL_UID + || callingUid == android.os.Process.ROOT_UID) { + return; // okay + } + + throw new SecurityException(message + ": Only shell user can call it"); + } } diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java new file mode 100644 index 000000000000..a76d8a31405d --- /dev/null +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence; + +import android.annotation.NonNull; +import android.os.ShellCommand; + +import java.io.PrintWriter; +import java.util.Objects; + +final class OnDeviceIntelligenceShellCommand extends ShellCommand { + private static final String TAG = OnDeviceIntelligenceShellCommand.class.getSimpleName(); + + @NonNull + private final OnDeviceIntelligenceManagerService mService; + + OnDeviceIntelligenceShellCommand(@NonNull OnDeviceIntelligenceManagerService service) { + mService = service; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + + switch (cmd) { + case "set-temporary-services": + return setTemporaryServices(); + case "get-services": + return getConfiguredServices(); + default: + return handleDefaultCommands(cmd); + } + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("OnDeviceIntelligenceShellCommand commands: "); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(); + pw.println( + " set-temporary-services [IntelligenceServiceComponentName] " + + "[InferenceServiceComponentName] [DURATION]"); + pw.println(" Temporarily (for DURATION ms) changes the service implementations."); + pw.println(" To reset, call without any arguments."); + + pw.println(" get-services To get the names of services that are currently being used."); + } + + private int setTemporaryServices() { + final PrintWriter out = getOutPrintWriter(); + final String intelligenceServiceName = getNextArg(); + final String inferenceServiceName = getNextArg(); + if (getRemainingArgsCount() == 0 && intelligenceServiceName == null + && inferenceServiceName == null) { + mService.resetTemporaryServices(); + out.println("OnDeviceIntelligenceManagerService temporary reset. "); + return 0; + } + + Objects.requireNonNull(intelligenceServiceName); + Objects.requireNonNull(inferenceServiceName); + final int duration = Integer.parseInt(getNextArgRequired()); + mService.setTemporaryServices( + new String[]{intelligenceServiceName, inferenceServiceName}, duration); + out.println("OnDeviceIntelligenceService temporarily set to " + intelligenceServiceName + + " \n and \n OnDeviceTrustedInferenceService set to " + inferenceServiceName + + " for " + duration + "ms"); + return 0; + } + + private int getConfiguredServices() { + final PrintWriter out = getOutPrintWriter(); + String[] services = mService.getServiceNames(); + out.println("OnDeviceIntelligenceService set to : " + services[0] + + " \n and \n OnDeviceTrustedInferenceService set to : " + services[1]); + return 0; + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 0069cdd1e4e8..6fa6957f2949 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -7786,8 +7786,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override void prepareSurfaces() { - final boolean show = isVisible() || isAnimating(PARENTS, - ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS + final boolean show = (isVisible() + // Ensure that the activity content is hidden when the decor surface is boosted to + // prevent UI redressing attack. + && !getTask().isDecorSurfaceBoosted()) + || isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS | ANIMATION_TYPE_PREDICT_BACK); if (mSurfaceControl != null) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d87e21c0ac92..55dc30cc37d5 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3741,7 +3741,9 @@ class Task extends TaskFragment { wc.assignChildLayers(t); if (!wc.needsZBoost()) { // Place the decor surface under any untrusted content. - if (mDecorSurfaceContainer != null && !decorSurfacePlaced + if (mDecorSurfaceContainer != null + && !mDecorSurfaceContainer.mIsBoosted + && !decorSurfacePlaced && shouldPlaceDecorSurfaceBelowContainer(wc)) { mDecorSurfaceContainer.assignLayer(t, layer++); decorSurfacePlaced = true; @@ -3760,7 +3762,9 @@ class Task extends TaskFragment { } // Place the decor surface just above the owner TaskFragment. - if (mDecorSurfaceContainer != null && !decorSurfacePlaced + if (mDecorSurfaceContainer != null + && !mDecorSurfaceContainer.mIsBoosted + && !decorSurfacePlaced && wc == mDecorSurfaceContainer.mOwnerTaskFragment) { mDecorSurfaceContainer.assignLayer(t, layer++); decorSurfacePlaced = true; @@ -3768,10 +3772,10 @@ class Task extends TaskFragment { } } - // If not placed yet, the decor surface should be on top of all non-boosted children. - if (mDecorSurfaceContainer != null && !decorSurfacePlaced) { + // Boost the decor surface above other non-boosted windows if requested. The cover surface + // will ensure that the content of the windows below are invisible. + if (mDecorSurfaceContainer != null && mDecorSurfaceContainer.mIsBoosted) { mDecorSurfaceContainer.assignLayer(t, layer++); - decorSurfacePlaced = true; } for (int j = 0; j < mChildren.size(); ++j) { @@ -3796,6 +3800,24 @@ class Task extends TaskFragment { return !isOwnActivity && !isTrustedTaskFragment; } + void setDecorSurfaceBoosted( + @NonNull TaskFragment ownerTaskFragment, + boolean isBoosted, + @Nullable SurfaceControl.Transaction clientTransaction) { + if (mDecorSurfaceContainer == null + || mDecorSurfaceContainer.mOwnerTaskFragment != ownerTaskFragment) { + return; + } + mDecorSurfaceContainer.setBoosted(isBoosted, clientTransaction); + // scheduleAnimation() is called inside assignChildLayers(), which ensures that child + // surface visibility is updated with prepareSurfaces() + assignChildLayers(); + } + + boolean isDecorSurfaceBoosted() { + return mDecorSurfaceContainer != null && mDecorSurfaceContainer.mIsBoosted; + } + boolean isTaskId(int taskId) { return mTaskId == taskId; } @@ -6796,14 +6818,35 @@ class Task extends TaskFragment { } /** - * A decor surface that is requested by a {@code TaskFragmentOrganizer} which will be placed - * below children windows except for own Activities and TaskFragment in fully trusted mode. + * A class managing the decor surface. + * + * A decor surface is requested by a {@link TaskFragmentOrganizer} and is placed below children + * windows in the Task except for own Activities and TaskFragments in fully trusted mode. The + * decor surface is created and shared with the client app with + * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE} and + * be removed with + * {@link android.window.TaskFragmentOperation#OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE}. + * + * When boosted with + * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_DECOR_SURFACE_BOOSTED}, the decor + * surface is placed above all non-boosted windows in the Task, but all the content below it + * will be hidden to prevent UI redressing attacks. This can be used by the draggable + * divider between {@link TaskFragment}s where veils are drawn on the decor surface while + * dragging to indicate new bounds. */ @VisibleForTesting class DecorSurfaceContainer { + + // The container surface is the parent of the decor surface. The container surface + // should NEVER be shared with the client. It is used to ensure that the decor surface has + // a z-order in the Task that is managed by WM core and cannot be updated by the client + // process. @VisibleForTesting @NonNull final SurfaceControl mContainerSurface; + // The decor surface is shared with the client process owning the + // {@link TaskFragmentOrganizer}. It can be used to draw the divider between TaskFragments + // or other decorations. @VisibleForTesting @NonNull final SurfaceControl mDecorSurface; @@ -6812,12 +6855,18 @@ class Task extends TaskFragment { @VisibleForTesting @NonNull TaskFragment mOwnerTaskFragment; + private boolean mIsBoosted; + + // The surface transactions that will be applied when the layer is reassigned. + @NonNull private final List<SurfaceControl.Transaction> mPendingClientTransactions = + new ArrayList<>(); + private DecorSurfaceContainer(@NonNull TaskFragment initialOwner) { mOwnerTaskFragment = initialOwner; mContainerSurface = makeSurface().setContainerLayer() .setParent(mSurfaceControl) .setName(mSurfaceControl + " - decor surface container") - .setEffectLayer() + .setContainerLayer() .setHidden(false) .setCallsite("Task.DecorSurfaceContainer") .build(); @@ -6830,14 +6879,28 @@ class Task extends TaskFragment { .build(); } + private void setBoosted( + boolean isBoosted, @Nullable SurfaceControl.Transaction clientTransaction) { + mIsBoosted = isBoosted; + // The client transaction will be applied together with the next assignLayer. + if (clientTransaction != null) { + mDecorSurfaceContainer.mPendingClientTransactions.add(clientTransaction); + } + } + private void assignLayer(@NonNull SurfaceControl.Transaction t, int layer) { t.setLayer(mContainerSurface, layer); t.setVisibility(mContainerSurface, mOwnerTaskFragment.isVisible()); + for (int i = 0; i < mPendingClientTransactions.size(); i++) { + t.merge(mPendingClientTransactions.get(i)); + } + mPendingClientTransactions.clear(); } private void release() { - mDecorSurface.release(); - mContainerSurface.release(); + getSyncTransaction() + .remove(mDecorSurface) + .remove(mContainerSurface); } } } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index a63e106beb55..7e6f5ac7497e 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -34,6 +34,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED; import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; import static android.window.TaskFragmentOperation.OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH; @@ -124,6 +125,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.pm.LauncherAppsService.LauncherAppsServiceInternal; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.HashMap; @@ -1557,13 +1559,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } case OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE: { - final Task task = taskFragment.getTask(); - task.moveOrCreateDecorSurfaceFor(taskFragment); + taskFragment.getTask().moveOrCreateDecorSurfaceFor(taskFragment); break; } case OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE: { - final Task task = taskFragment.getTask(); - task.removeDecorSurface(); + taskFragment.getTask().removeDecorSurface(); break; } case OP_TYPE_SET_DIM_ON_TASK: { @@ -1577,6 +1577,23 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub operation.isMoveToBottomIfClearWhenLaunch()); break; } + case OP_TYPE_SET_DECOR_SURFACE_BOOSTED: { + if (Flags.activityEmbeddingInteractiveDividerFlag()) { + final SurfaceControl.Transaction clientTransaction = + operation.getSurfaceTransaction(); + if (clientTransaction != null) { + // Sanitize the client transaction. sanitize() silently removes invalid + // operations and does not throw or provide signal about whether there are + // any invalid operations. + clientTransaction.sanitize(caller.mPid, caller.mUid); + } + taskFragment.getTask().setDecorSurfaceBoosted( + taskFragment, + operation.getBooleanValue() /* isBoosted */, + clientTransaction); + } + break; + } } return effects; } @@ -1616,19 +1633,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return false; } - // TODO (b/293654166) remove the decor surface checks once we clear security reviews - if ((opType == OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE - || opType == OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) - && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { - final Throwable exception = new SecurityException( - "Only a system organizer can perform OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE" - + " or OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE." - ); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - opType, exception); - return false; - } - if ((opType == OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH) && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { final Throwable exception = new SecurityException( diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index c778398342dc..610fcb5962c8 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -502,7 +502,7 @@ void NativeInputManager::dump(std::string& dump) { toString(mLocked.pointerGesturesEnabled)); dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches)); dump += StringPrintf(INDENT "Pointer Capture: %s, seq=%" PRIu32 "\n", - mLocked.pointerCaptureRequest.enable ? "Enabled" : "Disabled", + mLocked.pointerCaptureRequest.isEnable() ? "Enabled" : "Disabled", mLocked.pointerCaptureRequest.seq); if (auto pc = mLocked.legacyPointerController.lock(); pc) { dump += pc->dump(); @@ -1717,7 +1717,7 @@ void NativeInputManager::setPointerCapture(const PointerCaptureRequest& request) return; } - ALOGV("%s pointer capture.", request.enable ? "Enabling" : "Disabling"); + ALOGV("%s pointer capture.", request.isEnable() ? "Enabling" : "Disabling"); mLocked.pointerCaptureRequest = request; } // release lock diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 7c669f1aeb35..c6189ed405a1 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -161,6 +161,7 @@ import com.android.server.net.watchlist.NetworkWatchlistService; import com.android.server.notification.NotificationManagerService; import com.android.server.oemlock.OemLockService; import com.android.server.om.OverlayManagerService; +import com.android.server.ondeviceintelligence.OnDeviceIntelligenceManagerService; import com.android.server.os.BugreportManagerService; import com.android.server.os.DeviceIdentifiersPolicyService; import com.android.server.os.NativeTombstoneManagerService; @@ -1965,6 +1966,7 @@ public final class SystemServer implements Dumpable { startSystemCaptionsManagerService(context, t); startTextToSpeechManagerService(context, t); startWearableSensingService(t); + startOnDeviceIntelligenceService(t); if (deviceHasConfigString( context, R.string.config_defaultAmbientContextDetectionService)) { @@ -2166,16 +2168,14 @@ public final class SystemServer implements Dumpable { } t.traceEnd(); - if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { - t.traceBegin("StartVcnManagementService"); - try { - vcnManagement = VcnManagementService.create(context); - ServiceManager.addService(Context.VCN_MANAGEMENT_SERVICE, vcnManagement); - } catch (Throwable e) { - reportWtf("starting VCN Management Service", e); - } - t.traceEnd(); + t.traceBegin("StartVcnManagementService"); + try { + vcnManagement = VcnManagementService.create(context); + ServiceManager.addService(Context.VCN_MANAGEMENT_SERVICE, vcnManagement); + } catch (Throwable e) { + reportWtf("starting VCN Management Service", e); } + t.traceEnd(); t.traceBegin("StartSystemUpdateManagerService"); try { @@ -3337,6 +3337,12 @@ public final class SystemServer implements Dumpable { t.traceEnd(); // startOtherServices } + private void startOnDeviceIntelligenceService(TimingsTraceAndSlog t) { + t.traceBegin("startOnDeviceIntelligenceManagerService"); + mSystemServiceManager.startService(OnDeviceIntelligenceManagerService.class); + t.traceEnd(); + } + /** * Starts system services defined in apexes. * diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java index dc26e6e2374c..f6dc2f0f05b2 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java @@ -18,21 +18,23 @@ package com.android.server.accessibility; import static com.android.server.accessibility.AbstractAccessibilityServiceConnection.DISPLAY_TYPE_DEFAULT; import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.DisplayIdMatcher.displayId; +import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.WindowIdMatcher.windowId; import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.WindowChangesMatcher.a11yWindowChanges; -import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.WindowIdMatcher.a11yWindowId; +import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.EventWindowIdMatcher.eventWindowId; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -350,6 +352,88 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { } @Test + public void onWindowsChanged_shouldNotReportNonTouchableWindow() { + final AccessibilityWindow window = mWindows.get(Display.DEFAULT_DISPLAY).get(0); + when(window.isTouchable()).thenReturn(false); + final int windowId = mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, window.getWindowInfo().token); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, not(hasItem(windowId(windowId)))); + } + + @Test + public void onWindowsChanged_shouldReportFocusedNonTouchableWindow() { + final AccessibilityWindow window = mWindows.get(Display.DEFAULT_DISPLAY).get( + DEFAULT_FOCUSED_INDEX); + when(window.isTouchable()).thenReturn(false); + final int windowId = mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, window.getWindowInfo().token); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, hasItem(windowId(windowId))); + } + + @Test + public void onWindowsChanged_trustedFocusedNonTouchableWindow_shouldNotHideWindowsBelow() { + // Make the focused trusted un-touchable window fullscreen. + final AccessibilityWindow window = mWindows.get(Display.DEFAULT_DISPLAY).get( + DEFAULT_FOCUSED_INDEX); + setRegionForMockAccessibilityWindow(window, new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)); + when(window.isTouchable()).thenReturn(false); + when(window.isTrustedOverlay()).thenReturn(true); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, hasSize(NUM_OF_WINDOWS)); + } + + @Test + public void onWindowsChanged_accessibilityOverlay_shouldNotHideWindowsBelow() { + // Make the a11y overlay window fullscreen. + final AccessibilityWindow window = mWindows.get(Display.DEFAULT_DISPLAY).get(0); + setRegionForMockAccessibilityWindow(window, new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)); + when(window.getType()).thenReturn(WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, hasSize(NUM_OF_WINDOWS)); + } + + @Test + public void onWindowsChanged_shouldReportFocusedWindowEvenIfOccluded() { + // Make the front window fullscreen. + final AccessibilityWindow frontWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0); + setRegionForMockAccessibilityWindow(frontWindow, + new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)); + final int frontWindowId = mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, frontWindow.getWindowInfo().token); + + final AccessibilityWindow focusedWindow = mWindows.get(Display.DEFAULT_DISPLAY).get( + DEFAULT_FOCUSED_INDEX); + final int focusedWindowId = mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, focusedWindow.getWindowInfo().token); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, hasSize(2)); + assertThat(a11yWindows.get(0), windowId(frontWindowId)); + assertThat(a11yWindows.get(1), windowId(focusedWindowId)); + } + + @Test public void onWindowsChangedAndForceSend_shouldUpdateWindows() { assertNotEquals("new title", toString(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY) @@ -631,11 +715,11 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { .sendAccessibilityEventForCurrentUserLocked(captor.capture()); assertThat(captor.getAllValues().get(0), allOf(displayId(Display.DEFAULT_DISPLAY), - a11yWindowId(currentActiveWindowId), + eventWindowId(currentActiveWindowId), a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACTIVE))); assertThat(captor.getAllValues().get(1), allOf(displayId(Display.DEFAULT_DISPLAY), - a11yWindowId(eventWindowId), + eventWindowId(eventWindowId), a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACTIVE))); } @@ -661,7 +745,7 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { .sendAccessibilityEventForCurrentUserLocked(captor.capture()); assertThat(captor.getAllValues().get(0), allOf(displayId(Display.DEFAULT_DISPLAY), - a11yWindowId(eventWindowId), + eventWindowId(eventWindowId), a11yWindowChanges( AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED))); } @@ -710,12 +794,12 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { .sendAccessibilityEventForCurrentUserLocked(captor.capture()); assertThat(captor.getAllValues().get(0), allOf(displayId(initialDisplayId), - a11yWindowId(initialWindowId), + eventWindowId(initialWindowId), a11yWindowChanges( AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED))); assertThat(captor.getAllValues().get(1), allOf(displayId(eventDisplayId), - a11yWindowId(eventWindowId), + eventWindowId(eventWindowId), a11yWindowChanges( AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED))); } @@ -771,11 +855,11 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { .sendAccessibilityEventForCurrentUserLocked(captor.capture()); assertThat(captor.getAllValues().get(0), allOf(displayId(Display.DEFAULT_DISPLAY), - a11yWindowId(eventWindowId), + eventWindowId(eventWindowId), a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACTIVE))); assertThat(captor.getAllValues().get(1), allOf(displayId(Display.DEFAULT_DISPLAY), - a11yWindowId(currentActiveWindowId), + eventWindowId(currentActiveWindowId), a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACTIVE))); } @@ -979,7 +1063,7 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { .sendAccessibilityEventForCurrentUserLocked(captor.capture()); assertThat(captor.getAllValues().get(0), allOf(displayId(Display.DEFAULT_DISPLAY), - a11yWindowId(windowId), + eventWindowId(windowId), a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_REMOVED))); } @@ -1001,7 +1085,7 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { .sendAccessibilityEventForCurrentUserLocked(captor.capture()); assertThat(captor.getAllValues().get(0), allOf(displayId(Display.DEFAULT_DISPLAY), - a11yWindowId(windowId), + eventWindowId(windowId), a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ADDED))); } @@ -1019,7 +1103,7 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { .sendAccessibilityEventForCurrentUserLocked(captor.capture()); assertThat(captor.getAllValues().get(0), allOf(displayId(Display.DEFAULT_DISPLAY), - a11yWindowId(windowId), + eventWindowId(windowId), a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_TITLE))); } @@ -1173,8 +1257,6 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { private AccessibilityWindow createMockAccessibilityWindow(IWindow windowToken, int displayId) { final WindowInfo windowInfo = WindowInfo.obtain(); - // TODO(b/325341171): add tests with various kinds of windows such as - // changing window types, touchable or not, trusted or not, etc. windowInfo.type = WindowManager.LayoutParams.TYPE_APPLICATION; windowInfo.token = windowToken.asBinder(); @@ -1235,16 +1317,16 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { } } - static class WindowIdMatcher extends TypeSafeMatcher<AccessibilityEvent> { + static class EventWindowIdMatcher extends TypeSafeMatcher<AccessibilityEvent> { private int mWindowId; - WindowIdMatcher(int windowId) { + EventWindowIdMatcher(int windowId) { super(); mWindowId = windowId; } - static WindowIdMatcher a11yWindowId(int windowId) { - return new WindowIdMatcher(windowId); + static EventWindowIdMatcher eventWindowId(int windowId) { + return new EventWindowIdMatcher(windowId); } @Override @@ -1280,4 +1362,27 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { description.appendText("Matching to window changes " + mWindowChanges); } } + + static class WindowIdMatcher extends TypeSafeMatcher<AccessibilityWindowInfo> { + private final int mWindowId; + + WindowIdMatcher(int windowId) { + super(); + mWindowId = windowId; + } + + static WindowIdMatcher windowId(int windowId) { + return new WindowIdMatcher(windowId); + } + + @Override + protected boolean matchesSafely(AccessibilityWindowInfo window) { + return window.getId() == mWindowId; + } + + @Override + public void describeTo(Description description) { + description.appendText("Matching to windowId " + mWindowId); + } + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 961fdfb14bf3..f506e9fbbb14 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -1823,6 +1823,66 @@ public class TaskTests extends WindowTestsBase { } @Test + public void testAssignChildLayers_boostedDecorSurfacePlacement() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord unembeddedActivity = task.getTopMostActivity(); + + final TaskFragment fragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer); + final TaskFragment fragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer); + final SurfaceControl.Transaction t = task.getSyncTransaction(); + final SurfaceControl.Transaction clientTransaction = mock(SurfaceControl.Transaction.class); + + doNothing().when(task).sendTaskFragmentParentInfoChangedIfNeeded(); + spyOn(unembeddedActivity); + spyOn(fragment1); + spyOn(fragment2); + + doReturn(true).when(unembeddedActivity).isUid(task.effectiveUid); + doReturn(true).when(fragment1).isAllowedToBeEmbeddedInTrustedMode(); + doReturn(false).when(fragment2).isAllowedToBeEmbeddedInTrustedMode(); + doReturn(true).when(fragment1).isVisible(); + + task.moveOrCreateDecorSurfaceFor(fragment1); + + clearInvocations(t); + clearInvocations(unembeddedActivity); + clearInvocations(fragment1); + clearInvocations(fragment2); + + // The decor surface should be placed above all the windows when boosted and the cover + // surface should show. + task.setDecorSurfaceBoosted(fragment1, true /* isBoosted */, clientTransaction); + + verify(unembeddedActivity).assignLayer(t, 0); + verify(fragment1).assignLayer(t, 1); + verify(fragment2).assignLayer(t, 2); + verify(t).setLayer(task.mDecorSurfaceContainer.mContainerSurface, 3); + + verify(t).setVisibility(task.mDecorSurfaceContainer.mContainerSurface, true); + verify(t).merge(clientTransaction); + + clearInvocations(t); + clearInvocations(unembeddedActivity); + clearInvocations(fragment1); + clearInvocations(fragment2); + + // The decor surface should be placed just above the owner TaskFragment and the cover + // surface should hide. + task.moveOrCreateDecorSurfaceFor(fragment1); + task.setDecorSurfaceBoosted(fragment1, false /* isBoosted */, clientTransaction); + + verify(unembeddedActivity).assignLayer(t, 0); + verify(fragment1).assignLayer(t, 1); + verify(t).setLayer(task.mDecorSurfaceContainer.mContainerSurface, 2); + verify(fragment2).assignLayer(t, 3); + + verify(t).setVisibility(task.mDecorSurfaceContainer.mContainerSurface, true); + verify(t).merge(clientTransaction); + + } + + @Test public void testMoveTaskFragmentsToBottomIfNeeded() { final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); |