diff options
224 files changed, 8105 insertions, 2878 deletions
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index c4e8b0e49af3..a8b6c0b70804 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -105,6 +105,7 @@ static const char PROGRESS_PROP_NAME[] = "service.bootanim.progress"; static const char DISPLAYS_PROP_NAME[] = "persist.service.bootanim.displays"; static const char CLOCK_ENABLED_PROP_NAME[] = "persist.sys.bootanim.clock.enabled"; static const int ANIM_ENTRY_NAME_MAX = ANIM_PATH_MAX + 1; +static const int MAX_CHECK_EXIT_INTERVAL_US = 50000; static constexpr size_t TEXT_POS_LEN_MAX = 16; static const int DYNAMIC_COLOR_COUNT = 4; static const char U_TEXTURE[] = "uTexture"; @@ -1678,7 +1679,17 @@ bool BootAnimation::playAnimation(const Animation& animation) { checkExit(); } - usleep(part.pause * ns2us(frameDuration)); + int pauseDuration = part.pause * ns2us(frameDuration); + while(pauseDuration > 0 && !exitPending()){ + if (pauseDuration > MAX_CHECK_EXIT_INTERVAL_US) { + usleep(MAX_CHECK_EXIT_INTERVAL_US); + pauseDuration -= MAX_CHECK_EXIT_INTERVAL_US; + } else { + usleep(pauseDuration); + break; + } + checkExit(); + } if (exitPending() && !part.count && mCurrentInset >= mTargetInset && !part.hasFadingPhase()) { diff --git a/core/api/current.txt b/core/api/current.txt index 14d9a3e2279f..288ab479c0fb 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -13662,7 +13662,6 @@ package android.credentials { } public final class CredentialOption implements android.os.Parcelable { - ctor @Deprecated public CredentialOption(@NonNull String, @NonNull android.os.Bundle, @NonNull android.os.Bundle, boolean); method public int describeContents(); method @NonNull public java.util.Set<android.content.ComponentName> getAllowedProviders(); method @NonNull public android.os.Bundle getCandidateQueryData(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index e4e62a916db7..ace7d59c9a45 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10117,7 +10117,7 @@ package android.net.wifi.sharedconnectivity.app { public final class NetworkProviderInfo implements android.os.Parcelable { method public int describeContents(); method @IntRange(from=0, to=100) public int getBatteryPercentage(); - method @IntRange(from=0, to=3) public int getConnectionStrength(); + method @IntRange(from=0, to=4) public int getConnectionStrength(); method @NonNull public String getDeviceName(); method public int getDeviceType(); method @NonNull public android.os.Bundle getExtras(); @@ -10136,7 +10136,7 @@ package android.net.wifi.sharedconnectivity.app { ctor public NetworkProviderInfo.Builder(@NonNull String, @NonNull String); method @NonNull public android.net.wifi.sharedconnectivity.app.NetworkProviderInfo build(); method @NonNull public android.net.wifi.sharedconnectivity.app.NetworkProviderInfo.Builder setBatteryPercentage(@IntRange(from=0, to=100) int); - method @NonNull public android.net.wifi.sharedconnectivity.app.NetworkProviderInfo.Builder setConnectionStrength(@IntRange(from=0, to=3) int); + method @NonNull public android.net.wifi.sharedconnectivity.app.NetworkProviderInfo.Builder setConnectionStrength(@IntRange(from=0, to=4) int); method @NonNull public android.net.wifi.sharedconnectivity.app.NetworkProviderInfo.Builder setDeviceName(@NonNull String); method @NonNull public android.net.wifi.sharedconnectivity.app.NetworkProviderInfo.Builder setDeviceType(int); method @NonNull public android.net.wifi.sharedconnectivity.app.NetworkProviderInfo.Builder setExtras(@NonNull android.os.Bundle); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 2dfda517b495..9b5e31ac67be 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1651,6 +1651,11 @@ package android.hardware.soundtrigger { field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.KeyphraseMetadata> CREATOR; } + public class SoundTrigger { + field public static final int MODEL_PARAM_INVALID = -1; // 0xffffffff + field public static final int MODEL_PARAM_THRESHOLD_FACTOR = 0; // 0x0 + } + public static final class SoundTrigger.KeyphraseRecognitionExtra implements android.os.Parcelable { ctor public SoundTrigger.KeyphraseRecognitionExtra(int, int, int); } @@ -1663,6 +1668,19 @@ package android.hardware.soundtrigger { ctor public SoundTrigger.ModuleProperties(int, @NonNull String, @NonNull String, @NonNull String, int, @NonNull String, int, int, int, int, boolean, int, boolean, int, boolean, int); } + public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable { + ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int); + ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[]); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR; + field public final boolean allowMultipleTriggers; + field public final int audioCapabilities; + field public final boolean captureRequested; + field @NonNull public final byte[] data; + field @NonNull public final android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[] keyphrases; + } + public static class SoundTrigger.RecognitionEvent { ctor public SoundTrigger.RecognitionEvent(int, int, boolean, int, int, int, boolean, @NonNull android.media.AudioFormat, @Nullable byte[], long); } @@ -2000,6 +2018,57 @@ package android.media.metrics { } +package android.media.soundtrigger { + + public final class SoundTriggerInstrumentation { + method public void setResourceContention(boolean); + method public void triggerOnResourcesAvailable(); + method public void triggerRestart(); + } + + public static interface SoundTriggerInstrumentation.GlobalCallback { + method public default void onClientAttached(); + method public default void onClientDetached(); + method public default void onFrameworkDetached(); + method public void onModelLoaded(@NonNull android.media.soundtrigger.SoundTriggerInstrumentation.ModelSession); + method public default void onPreempted(); + method public default void onRestarted(); + } + + public static interface SoundTriggerInstrumentation.ModelCallback { + method public default void onModelUnloaded(); + method public default void onParamSet(int, int); + method public void onRecognitionStarted(@NonNull android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionSession); + } + + public class SoundTriggerInstrumentation.ModelSession { + method public void clearModelCallback(); + method @NonNull public java.util.List<android.hardware.soundtrigger.SoundTrigger.Keyphrase> getPhrases(); + method @NonNull public android.media.soundtrigger.SoundTriggerManager.Model getSoundModel(); + method public boolean isKeyphrase(); + method public void setModelCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.soundtrigger.SoundTriggerInstrumentation.ModelCallback); + method public void triggerUnloadModel(); + } + + public static interface SoundTriggerInstrumentation.RecognitionCallback { + method public void onRecognitionStopped(); + } + + public class SoundTriggerInstrumentation.RecognitionSession { + method public void clearRecognitionCallback(); + method public int getAudioSession(); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig getRecognitionConfig(); + method public void setRecognitionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionCallback); + method public void triggerAbortRecognition(); + method public void triggerRecognitionEvent(@NonNull byte[], @Nullable java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>); + } + + public final class SoundTriggerManager { + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public static android.media.soundtrigger.SoundTriggerInstrumentation attachInstrumentation(@NonNull java.util.concurrent.Executor, @NonNull android.media.soundtrigger.SoundTriggerInstrumentation.GlobalCallback); + } + +} + package android.media.tv { public final class TvInputManager { @@ -2024,6 +2093,14 @@ package android.media.tv.tuner { } +package android.media.voice { + + public final class KeyphraseModelManager { + method @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public void setModelDatabaseForTestEnabled(boolean); + } + +} + package android.net { public class NetworkPolicyManager { @@ -2944,6 +3021,7 @@ package android.service.voice { method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetectorForTest(@NonNull String, @NonNull java.util.Locale, @NonNull android.hardware.soundtrigger.SoundTrigger.ModuleProperties, @NonNull java.util.concurrent.Executor, @NonNull android.service.voice.AlwaysOnHotwordDetector.Callback); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetectorForTest(@NonNull String, @NonNull java.util.Locale, @Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull android.hardware.soundtrigger.SoundTrigger.ModuleProperties, @NonNull java.util.concurrent.Executor, @NonNull android.service.voice.AlwaysOnHotwordDetector.Callback); method @NonNull public final java.util.List<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> listModuleProperties(); + method public final void setTestModuleForAlwaysOnHotwordDetectorEnabled(boolean); } public static class VoiceInteractionSession.ActivityId { diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 0293bb53d3f0..95e446dde4da 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -343,7 +343,170 @@ public abstract class ActivityManagerInternal { */ public abstract boolean hasRunningActivity(int uid, @Nullable String packageName); - public abstract void updateOomAdj(); + /** + * Oom Adj Reason: none - internal use only, do not use it. + * @hide + */ + public static final int OOM_ADJ_REASON_NONE = 0; + + /** + * Oom Adj Reason: activity changes. + * @hide + */ + public static final int OOM_ADJ_REASON_ACTIVITY = 1; + + /** + * Oom Adj Reason: finishing a broadcast receiver. + * @hide + */ + public static final int OOM_ADJ_REASON_FINISH_RECEIVER = 2; + + /** + * Oom Adj Reason: starting a broadcast receiver. + * @hide + */ + public static final int OOM_ADJ_REASON_START_RECEIVER = 3; + + /** + * Oom Adj Reason: binding to a service. + * @hide + */ + public static final int OOM_ADJ_REASON_BIND_SERVICE = 4; + + /** + * Oom Adj Reason: unbinding from a service. + * @hide + */ + public static final int OOM_ADJ_REASON_UNBIND_SERVICE = 5; + + /** + * Oom Adj Reason: starting a service. + * @hide + */ + public static final int OOM_ADJ_REASON_START_SERVICE = 6; + + /** + * Oom Adj Reason: connecting to a content provider. + * @hide + */ + public static final int OOM_ADJ_REASON_GET_PROVIDER = 7; + + /** + * Oom Adj Reason: disconnecting from a content provider. + * @hide + */ + public static final int OOM_ADJ_REASON_REMOVE_PROVIDER = 8; + + /** + * Oom Adj Reason: UI visibility changes. + * @hide + */ + public static final int OOM_ADJ_REASON_UI_VISIBILITY = 9; + + /** + * Oom Adj Reason: device power allowlist changes. + * @hide + */ + public static final int OOM_ADJ_REASON_ALLOWLIST = 10; + + /** + * Oom Adj Reason: starting a process. + * @hide + */ + public static final int OOM_ADJ_REASON_PROCESS_BEGIN = 11; + + /** + * Oom Adj Reason: ending a process. + * @hide + */ + public static final int OOM_ADJ_REASON_PROCESS_END = 12; + + /** + * Oom Adj Reason: short FGS timeout. + * @hide + */ + public static final int OOM_ADJ_REASON_SHORT_FGS_TIMEOUT = 13; + + /** + * Oom Adj Reason: system initialization. + * @hide + */ + public static final int OOM_ADJ_REASON_SYSTEM_INIT = 14; + + /** + * Oom Adj Reason: backup/restore. + * @hide + */ + public static final int OOM_ADJ_REASON_BACKUP = 15; + + /** + * Oom Adj Reason: instrumented by the SHELL. + * @hide + */ + public static final int OOM_ADJ_REASON_SHELL = 16; + + /** + * Oom Adj Reason: task stack is being removed. + */ + public static final int OOM_ADJ_REASON_REMOVE_TASK = 17; + + /** + * Oom Adj Reason: uid idle. + */ + public static final int OOM_ADJ_REASON_UID_IDLE = 18; + + /** + * Oom Adj Reason: stop service. + */ + public static final int OOM_ADJ_REASON_STOP_SERVICE = 19; + + /** + * Oom Adj Reason: executing service. + */ + public static final int OOM_ADJ_REASON_EXECUTING_SERVICE = 20; + + /** + * Oom Adj Reason: background restriction changes. + */ + public static final int OOM_ADJ_REASON_RESTRICTION_CHANGE = 21; + + /** + * Oom Adj Reason: A package or its component is disabled. + */ + public static final int OOM_ADJ_REASON_COMPONENT_DISABLED = 22; + + @IntDef(prefix = {"OOM_ADJ_REASON_"}, value = { + OOM_ADJ_REASON_NONE, + OOM_ADJ_REASON_ACTIVITY, + OOM_ADJ_REASON_FINISH_RECEIVER, + OOM_ADJ_REASON_START_RECEIVER, + OOM_ADJ_REASON_BIND_SERVICE, + OOM_ADJ_REASON_UNBIND_SERVICE, + OOM_ADJ_REASON_START_SERVICE, + OOM_ADJ_REASON_GET_PROVIDER, + OOM_ADJ_REASON_REMOVE_PROVIDER, + OOM_ADJ_REASON_UI_VISIBILITY, + OOM_ADJ_REASON_ALLOWLIST, + OOM_ADJ_REASON_PROCESS_BEGIN, + OOM_ADJ_REASON_PROCESS_END, + OOM_ADJ_REASON_SHORT_FGS_TIMEOUT, + OOM_ADJ_REASON_SYSTEM_INIT, + OOM_ADJ_REASON_BACKUP, + OOM_ADJ_REASON_SHELL, + OOM_ADJ_REASON_REMOVE_TASK, + OOM_ADJ_REASON_UID_IDLE, + OOM_ADJ_REASON_STOP_SERVICE, + OOM_ADJ_REASON_EXECUTING_SERVICE, + OOM_ADJ_REASON_RESTRICTION_CHANGE, + OOM_ADJ_REASON_COMPONENT_DISABLED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface OomAdjReason {} + + /** + * Request to update oom adj. + */ + public abstract void updateOomAdj(@OomAdjReason int oomAdjReason); public abstract void updateCpuStats(); /** diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index b48a8fb73832..1dddf064e82a 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1461,9 +1461,25 @@ public class AppOpsManager { */ public static final int OP_USE_FULL_SCREEN_INTENT = AppProtoEnums.APP_OP_USE_FULL_SCREEN_INTENT; + /** + * Hides camera indicator for sandboxed detection apps that directly access the service. + * + * @hide + */ + public static final int OP_CAMERA_SANDBOXED = + AppProtoEnums.APP_OP_CAMERA_SANDBOXED; + + /** + * Hides microphone indicator for sandboxed detection apps that directly access the service. + * + * @hide + */ + public static final int OP_RECORD_AUDIO_SANDBOXED = + AppProtoEnums.APP_OP_RECORD_AUDIO_SANDBOXED; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 134; + public static final int _NUM_OP = 136; /** * All app ops represented as strings. @@ -1605,6 +1621,8 @@ public class AppOpsManager { OPSTR_CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD, OPSTR_BODY_SENSORS_WRIST_TEMPERATURE, OPSTR_USE_FULL_SCREEN_INTENT, + OPSTR_CAMERA_SANDBOXED, + OPSTR_RECORD_AUDIO_SANDBOXED }) public @interface AppOpString {} @@ -2013,6 +2031,20 @@ public class AppOpsManager { public static final String OPSTR_COARSE_LOCATION_SOURCE = "android:coarse_location_source"; /** + * Camera is being recorded in sandboxed detection process. + * + * @hide + */ + public static final String OPSTR_CAMERA_SANDBOXED = "android:camera_sandboxed"; + + /** + * Audio is being recorded in sandboxed detection process. + * + * @hide + */ + public static final String OPSTR_RECORD_AUDIO_SANDBOXED = "android:record_audio_sandboxed"; + + /** * Allow apps to create the requests to manage the media files without user confirmation. * * @see android.Manifest.permission#MANAGE_MEDIA @@ -2738,7 +2770,11 @@ public class AppOpsManager { .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), new AppOpInfo.Builder(OP_USE_FULL_SCREEN_INTENT, OPSTR_USE_FULL_SCREEN_INTENT, "USE_FULL_SCREEN_INTENT").setPermission(Manifest.permission.USE_FULL_SCREEN_INTENT) - .build() + .build(), + new AppOpInfo.Builder(OP_CAMERA_SANDBOXED, OPSTR_CAMERA_SANDBOXED, + "CAMERA_SANDBOXED").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), + new AppOpInfo.Builder(OP_RECORD_AUDIO_SANDBOXED, OPSTR_RECORD_AUDIO_SANDBOXED, + "RECORD_AUDIO_SANDBOXED").setDefaultMode(AppOpsManager.MODE_ALLOWED).build() }; // The number of longs needed to form a full bitmask of app ops diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 746b8f70b6af..0b4862176040 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -24,6 +24,7 @@ import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationHistory; import android.app.NotificationManager; +import android.content.AttributionSource; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ParceledListSlice; @@ -225,6 +226,7 @@ interface INotificationManager void setNotificationDelegate(String callingPkg, String delegate); String getNotificationDelegate(String callingPkg); boolean canNotifyAsPackage(String callingPkg, String targetPkg, int userId); + boolean canUseFullScreenIntent(in AttributionSource attributionSource); void setPrivateNotificationsAllowed(boolean allow); boolean getPrivateNotificationsAllowed(); diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 80f64e03afe8..785470f2f22e 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -31,7 +31,6 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.PermissionChecker; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; import android.graphics.drawable.Icon; @@ -877,19 +876,11 @@ public class NotificationManager { * {@link android.provider.Settings#ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT}. */ public boolean canUseFullScreenIntent() { - final int result = PermissionChecker.checkPermissionForPreflight(mContext, - android.Manifest.permission.USE_FULL_SCREEN_INTENT, - mContext.getAttributionSource()); - - switch (result) { - case PermissionChecker.PERMISSION_GRANTED: - return true; - case PermissionChecker.PERMISSION_SOFT_DENIED: - case PermissionChecker.PERMISSION_HARD_DENIED: - return false; - default: - if (localLOGV) Log.v(TAG, "Unknown PermissionChecker result: " + result); - return false; + INotificationManager service = getService(); + try { + return service.canUseFullScreenIntent(mContext.getAttributionSource()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } diff --git a/core/java/android/credentials/CredentialOption.java b/core/java/android/credentials/CredentialOption.java index e933123d08b8..df948f17d6e0 100644 --- a/core/java/android/credentials/CredentialOption.java +++ b/core/java/android/credentials/CredentialOption.java @@ -37,8 +37,7 @@ import java.util.Set; /** * Information about a specific type of credential to be requested during a {@link - * CredentialManager#getCredential(GetCredentialRequest, Activity, CancellationSignal, Executor, - * OutcomeReceiver)} operation. + * CredentialManager#getCredential} operation. */ public final class CredentialOption implements Parcelable { @@ -196,9 +195,8 @@ public final class CredentialOption implements Parcelable { * @throws NullPointerException If {@code credentialRetrievalData}, or * {@code candidateQueryData} is null. * - * @deprecated replaced by Builder + * @hide */ - @Deprecated public CredentialOption( @NonNull String type, @NonNull Bundle credentialRetrievalData, diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index fa678fc5ee1a..2e40f6096ccb 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -142,6 +142,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan private PromptInfo mPromptInfo; private ButtonInfo mNegativeButtonInfo; private Context mContext; + private IAuthService mService; /** * Creates a builder for a {@link BiometricPrompt} dialog. @@ -212,6 +213,18 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** + * @param service + * @return This builder. + * @hide + */ + @RequiresPermission(TEST_BIOMETRIC) + @NonNull + public Builder setService(@NonNull IAuthService service) { + mService = service; + return this; + } + + /** * Sets an optional title, subtitle, and/or description that will override other text when * the user is authenticating with PIN/pattern/password. Currently for internal use only. * @return This builder. @@ -472,7 +485,9 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan throw new IllegalArgumentException("Can't have both negative button behavior" + " and device credential enabled"); } - return new BiometricPrompt(mContext, mPromptInfo, mNegativeButtonInfo); + mService = (mService == null) ? IAuthService.Stub.asInterface( + ServiceManager.getService(Context.AUTH_SERVICE)) : mService; + return new BiometricPrompt(mContext, mPromptInfo, mNegativeButtonInfo, mService); } } @@ -521,7 +536,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan public void onAuthenticationFailed() { mExecutor.execute(() -> { mAuthenticationCallback.onAuthenticationFailed(); - mIsPromptShowing = false; }); } @@ -604,12 +618,12 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan private boolean mIsPromptShowing; - private BiometricPrompt(Context context, PromptInfo promptInfo, ButtonInfo negativeButtonInfo) { + private BiometricPrompt(Context context, PromptInfo promptInfo, ButtonInfo negativeButtonInfo, + IAuthService service) { mContext = context; mPromptInfo = promptInfo; mNegativeButtonInfo = negativeButtonInfo; - mService = IAuthService.Stub.asInterface( - ServiceManager.getService(Context.AUTH_SERVICE)); + mService = service; mIsPromptShowing = false; } diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index fa16e167f7d1..6d43ddf7fe94 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -1051,6 +1051,29 @@ public class SoundTrigger { return "ModelParamRange [start=" + mStart + ", end=" + mEnd + "]"; } } + /** + * SoundTrigger model parameter types. + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "MODEL_PARAM" }, value = { + MODEL_PARAM_INVALID, + MODEL_PARAM_THRESHOLD_FACTOR + }) + public @interface ModelParamTypes {} + + /** + * See {@link ModelParams.INVALID} + * @hide + */ + @TestApi + public static final int MODEL_PARAM_INVALID = ModelParams.INVALID; + /** + * See {@link ModelParams.THRESHOLD_FACTOR} + * @hide + */ + @TestApi + public static final int MODEL_PARAM_THRESHOLD_FACTOR = ModelParams.THRESHOLD_FACTOR; /** * Modes for key phrase recognition @@ -1450,7 +1473,8 @@ public class SoundTrigger { * * @hide */ - public static class RecognitionConfig implements Parcelable { + @TestApi + public static final class RecognitionConfig implements Parcelable { /** True if the DSP should capture the trigger sound and make it available for further * capture. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -1464,6 +1488,7 @@ public class SoundTrigger { * options for each keyphrase. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @NonNull + @SuppressLint("ArrayReturn") public final KeyphraseRecognitionExtra keyphrases[]; /** Opaque data for use by system applications who know about voice engine internals, * typically during enrollment. */ @@ -1479,8 +1504,8 @@ public class SoundTrigger { public final int audioCapabilities; public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, - @Nullable KeyphraseRecognitionExtra[] keyphrases, @Nullable byte[] data, - int audioCapabilities) { + @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases, + @Nullable byte[] data, int audioCapabilities) { this.captureRequested = captureRequested; this.allowMultipleTriggers = allowMultipleTriggers; this.keyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0]; @@ -1490,7 +1515,8 @@ public class SoundTrigger { @UnsupportedAppUsage public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, - @Nullable KeyphraseRecognitionExtra[] keyphrases, @Nullable byte[] data) { + @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases, + @Nullable byte[] data) { this(captureRequested, allowMultipleTriggers, keyphrases, data, 0); } @@ -1517,7 +1543,7 @@ public class SoundTrigger { } @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeByte((byte) (captureRequested ? 1 : 0)); dest.writeByte((byte) (allowMultipleTriggers ? 1 : 0)); dest.writeTypedArray(keyphrases, flags); diff --git a/core/java/android/permission/OWNERS b/core/java/android/permission/OWNERS index d34b45bf1ff1..4603e43fd164 100644 --- a/core/java/android/permission/OWNERS +++ b/core/java/android/permission/OWNERS @@ -1,18 +1,19 @@ # Bug component: 137825 -evanseverson@google.com -evanxinchen@google.com ashfall@google.com -guojing@google.com +augale@google.com +evanseverson@google.com +fayey@google.com jaysullivan@google.com +joecastro@google.com kvakil@google.com mrulhania@google.com narayan@google.com ntmyren@google.com olekarg@google.com pyuli@google.com -raphk@google.com rmacgregor@google.com sergeynv@google.com theianchen@google.com +yutingfang@google.com zhanghai@google.com diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 402da28b3c5c..828c062d955d 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -45,7 +45,6 @@ import android.provider.Settings.Global; import android.text.TextUtils; import android.text.format.DateFormat; import android.util.ArrayMap; -import android.util.ArraySet; import android.util.PluralsMessageFormatter; import android.util.Slog; import android.util.proto.ProtoOutputStream; @@ -59,7 +58,6 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; @@ -310,86 +308,6 @@ public class ZenModeConfig implements Parcelable { return buffer.toString(); } - public Diff diff(ZenModeConfig to) { - final Diff d = new Diff(); - if (to == null) { - return d.addLine("config", "delete"); - } - if (user != to.user) { - d.addLine("user", user, to.user); - } - if (allowAlarms != to.allowAlarms) { - d.addLine("allowAlarms", allowAlarms, to.allowAlarms); - } - if (allowMedia != to.allowMedia) { - d.addLine("allowMedia", allowMedia, to.allowMedia); - } - if (allowSystem != to.allowSystem) { - d.addLine("allowSystem", allowSystem, to.allowSystem); - } - if (allowCalls != to.allowCalls) { - d.addLine("allowCalls", allowCalls, to.allowCalls); - } - if (allowReminders != to.allowReminders) { - d.addLine("allowReminders", allowReminders, to.allowReminders); - } - if (allowEvents != to.allowEvents) { - d.addLine("allowEvents", allowEvents, to.allowEvents); - } - if (allowRepeatCallers != to.allowRepeatCallers) { - d.addLine("allowRepeatCallers", allowRepeatCallers, to.allowRepeatCallers); - } - if (allowMessages != to.allowMessages) { - d.addLine("allowMessages", allowMessages, to.allowMessages); - } - if (allowCallsFrom != to.allowCallsFrom) { - d.addLine("allowCallsFrom", allowCallsFrom, to.allowCallsFrom); - } - if (allowMessagesFrom != to.allowMessagesFrom) { - d.addLine("allowMessagesFrom", allowMessagesFrom, to.allowMessagesFrom); - } - if (suppressedVisualEffects != to.suppressedVisualEffects) { - d.addLine("suppressedVisualEffects", suppressedVisualEffects, - to.suppressedVisualEffects); - } - final ArraySet<String> allRules = new ArraySet<>(); - addKeys(allRules, automaticRules); - addKeys(allRules, to.automaticRules); - final int N = allRules.size(); - for (int i = 0; i < N; i++) { - final String rule = allRules.valueAt(i); - final ZenRule fromRule = automaticRules != null ? automaticRules.get(rule) : null; - final ZenRule toRule = to.automaticRules != null ? to.automaticRules.get(rule) : null; - ZenRule.appendDiff(d, "automaticRule[" + rule + "]", fromRule, toRule); - } - ZenRule.appendDiff(d, "manualRule", manualRule, to.manualRule); - - if (areChannelsBypassingDnd != to.areChannelsBypassingDnd) { - d.addLine("areChannelsBypassingDnd", areChannelsBypassingDnd, - to.areChannelsBypassingDnd); - } - return d; - } - - public static Diff diff(ZenModeConfig from, ZenModeConfig to) { - if (from == null) { - final Diff d = new Diff(); - if (to != null) { - d.addLine("config", "insert"); - } - return d; - } - return from.diff(to); - } - - private static <T> void addKeys(ArraySet<T> set, ArrayMap<T, ?> map) { - if (map != null) { - for (int i = 0; i < map.size(); i++) { - set.add(map.keyAt(i)); - } - } - } - public boolean isValid() { if (!isValidManualRule(manualRule)) return false; final int N = automaticRules.size(); @@ -1922,66 +1840,6 @@ public class ZenModeConfig implements Parcelable { proto.end(token); } - private static void appendDiff(Diff d, String item, ZenRule from, ZenRule to) { - if (d == null) return; - if (from == null) { - if (to != null) { - d.addLine(item, "insert"); - } - return; - } - from.appendDiff(d, item, to); - } - - private void appendDiff(Diff d, String item, ZenRule to) { - if (to == null) { - d.addLine(item, "delete"); - return; - } - if (enabled != to.enabled) { - d.addLine(item, "enabled", enabled, to.enabled); - } - if (snoozing != to.snoozing) { - d.addLine(item, "snoozing", snoozing, to.snoozing); - } - if (!Objects.equals(name, to.name)) { - d.addLine(item, "name", name, to.name); - } - if (zenMode != to.zenMode) { - d.addLine(item, "zenMode", zenMode, to.zenMode); - } - if (!Objects.equals(conditionId, to.conditionId)) { - d.addLine(item, "conditionId", conditionId, to.conditionId); - } - if (!Objects.equals(condition, to.condition)) { - d.addLine(item, "condition", condition, to.condition); - } - if (!Objects.equals(component, to.component)) { - d.addLine(item, "component", component, to.component); - } - if (!Objects.equals(configurationActivity, to.configurationActivity)) { - d.addLine(item, "configActivity", configurationActivity, to.configurationActivity); - } - if (!Objects.equals(id, to.id)) { - d.addLine(item, "id", id, to.id); - } - if (creationTime != to.creationTime) { - d.addLine(item, "creationTime", creationTime, to.creationTime); - } - if (!Objects.equals(enabler, to.enabler)) { - d.addLine(item, "enabler", enabler, to.enabler); - } - if (!Objects.equals(zenPolicy, to.zenPolicy)) { - d.addLine(item, "zenPolicy", zenPolicy, to.zenPolicy); - } - if (modified != to.modified) { - d.addLine(item, "modified", modified, to.modified); - } - if (!Objects.equals(pkg, to.pkg)) { - d.addLine(item, "pkg", pkg, to.pkg); - } - } - @Override public boolean equals(@Nullable Object o) { if (!(o instanceof ZenRule)) return false; @@ -2040,40 +1898,6 @@ public class ZenModeConfig implements Parcelable { }; } - public static class Diff { - private final ArrayList<String> lines = new ArrayList<>(); - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Diff["); - final int N = lines.size(); - for (int i = 0; i < N; i++) { - if (i > 0) { - sb.append(",\n"); - } - sb.append(lines.get(i)); - } - return sb.append(']').toString(); - } - - private Diff addLine(String item, String action) { - lines.add(item + ":" + action); - return this; - } - - public Diff addLine(String item, String subitem, Object from, Object to) { - return addLine(item + "." + subitem, from, to); - } - - public Diff addLine(String item, Object from, Object to) { - return addLine(item, from + "->" + to); - } - - public boolean isEmpty() { - return lines.isEmpty(); - } - } - /** * Determines whether dnd behavior should mute all ringer-controlled sounds * This includes notification, ringer and system sounds diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java new file mode 100644 index 000000000000..c7b89eb284b6 --- /dev/null +++ b/core/java/android/service/notification/ZenModeDiff.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.notification; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.ArraySet; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.Set; + +/** + * ZenModeDiff is a utility class meant to encapsulate the diff between ZenModeConfigs and their + * subcomponents (automatic and manual ZenRules). + * @hide + */ +public class ZenModeDiff { + /** + * Enum representing whether the existence of a config or rule has changed (added or removed, + * or "none" meaning there is no change, which may either mean both null, or there exists a + * diff in fields rather than add/remove). + */ + @IntDef(value = { + NONE, + ADDED, + REMOVED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ExistenceChange{} + + public static final int NONE = 0; + public static final int ADDED = 1; + public static final int REMOVED = 2; + + /** + * Diff class representing an individual field diff. + * @param <T> The type of the field. + */ + public static class FieldDiff<T> { + private final T mFrom; + private final T mTo; + + /** + * Constructor to create a FieldDiff object with the given values. + * @param from from (old) value + * @param to to (new) value + */ + public FieldDiff(@Nullable T from, @Nullable T to) { + mFrom = from; + mTo = to; + } + + /** + * Get the "from" value + */ + public T from() { + return mFrom; + } + + /** + * Get the "to" value + */ + public T to() { + return mTo; + } + + /** + * Get the string representation of this field diff, in the form of "from->to". + */ + @Override + public String toString() { + return mFrom + "->" + mTo; + } + + /** + * Returns whether this represents an actual diff. + */ + public boolean hasDiff() { + // note that Objects.equals handles null values gracefully. + return !Objects.equals(mFrom, mTo); + } + } + + /** + * Base diff class that contains info about whether something was added, and a set of named + * fields that changed. + * Extend for diffs of specific types of objects. + */ + private abstract static class BaseDiff { + // Whether the diff was added or removed + @ExistenceChange private int mExists = NONE; + + // Map from field name to diffs for any standalone fields in the object. + private ArrayMap<String, FieldDiff> mFields = new ArrayMap<>(); + + // Functions for actually diffing objects and string representations have to be implemented + // by subclasses. + + /** + * Return whether this diff represents any changes. + */ + public abstract boolean hasDiff(); + + /** + * Return a string representation of the diff. + */ + public abstract String toString(); + + /** + * Constructor that takes the two objects meant to be compared. This constructor sets + * whether there is an existence change (added or removed). + * @param from previous Object + * @param to new Object + */ + BaseDiff(Object from, Object to) { + if (from == null) { + if (to != null) { + mExists = ADDED; + } + // If both are null, there isn't an existence change; callers/inheritors must handle + // the both null case. + } else if (to == null) { + // in this case, we know that from != null + mExists = REMOVED; + } + + // Subclasses should implement the actual diffing functionality in their own + // constructors. + } + + /** + * Add a diff for a specific field to the map. + * @param name field name + * @param diff FieldDiff object representing the diff + */ + final void addField(String name, FieldDiff diff) { + mFields.put(name, diff); + } + + /** + * Returns whether this diff represents a config being newly added. + */ + public final boolean wasAdded() { + return mExists == ADDED; + } + + /** + * Returns whether this diff represents a config being removed. + */ + public final boolean wasRemoved() { + return mExists == REMOVED; + } + + /** + * Returns whether this diff represents an object being either added or removed. + */ + public final boolean hasExistenceChange() { + return mExists != NONE; + } + + /** + * Returns whether there are any individual field diffs. + */ + public final boolean hasFieldDiffs() { + return mFields.size() > 0; + } + + /** + * Returns the diff for the specific named field if it exists + */ + public final FieldDiff getDiffForField(String name) { + return mFields.getOrDefault(name, null); + } + + /** + * Get the set of all field names with some diff. + */ + public final Set<String> fieldNamesWithDiff() { + return mFields.keySet(); + } + } + + /** + * Diff class representing a diff between two ZenModeConfigs. + */ + public static class ConfigDiff extends BaseDiff { + // Rules. Automatic rule map is keyed by the rule name. + private final ArrayMap<String, RuleDiff> mAutomaticRulesDiff = new ArrayMap<>(); + private RuleDiff mManualRuleDiff; + + // Helpers for string generation + private static final String ALLOW_CALLS_FROM_FIELD = "allowCallsFrom"; + private static final String ALLOW_MESSAGES_FROM_FIELD = "allowMessagesFrom"; + private static final String ALLOW_CONVERSATIONS_FROM_FIELD = "allowConversationsFrom"; + private static final Set<String> PEOPLE_TYPE_FIELDS = + Set.of(ALLOW_CALLS_FROM_FIELD, ALLOW_MESSAGES_FROM_FIELD); + + /** + * Create a diff that contains diffs between the "from" and "to" ZenModeConfigs. + * + * @param from previous ZenModeConfig + * @param to new ZenModeConfig + */ + public ConfigDiff(ZenModeConfig from, ZenModeConfig to) { + super(from, to); + // If both are null skip + if (from == null && to == null) { + return; + } + if (hasExistenceChange()) { + // either added or removed; return here. otherwise (they're not both null) there's + // field diffs. + return; + } + + // Now we compare all the fields, knowing there's a diff and that neither is null + if (from.user != to.user) { + addField("user", new FieldDiff<>(from.user, to.user)); + } + if (from.allowAlarms != to.allowAlarms) { + addField("allowAlarms", new FieldDiff<>(from.allowAlarms, to.allowAlarms)); + } + if (from.allowMedia != to.allowMedia) { + addField("allowMedia", new FieldDiff<>(from.allowMedia, to.allowMedia)); + } + if (from.allowSystem != to.allowSystem) { + addField("allowSystem", new FieldDiff<>(from.allowSystem, to.allowSystem)); + } + if (from.allowCalls != to.allowCalls) { + addField("allowCalls", new FieldDiff<>(from.allowCalls, to.allowCalls)); + } + if (from.allowReminders != to.allowReminders) { + addField("allowReminders", + new FieldDiff<>(from.allowReminders, to.allowReminders)); + } + if (from.allowEvents != to.allowEvents) { + addField("allowEvents", new FieldDiff<>(from.allowEvents, to.allowEvents)); + } + if (from.allowRepeatCallers != to.allowRepeatCallers) { + addField("allowRepeatCallers", + new FieldDiff<>(from.allowRepeatCallers, to.allowRepeatCallers)); + } + if (from.allowMessages != to.allowMessages) { + addField("allowMessages", + new FieldDiff<>(from.allowMessages, to.allowMessages)); + } + if (from.allowConversations != to.allowConversations) { + addField("allowConversations", + new FieldDiff<>(from.allowConversations, to.allowConversations)); + } + if (from.allowCallsFrom != to.allowCallsFrom) { + addField("allowCallsFrom", + new FieldDiff<>(from.allowCallsFrom, to.allowCallsFrom)); + } + if (from.allowMessagesFrom != to.allowMessagesFrom) { + addField("allowMessagesFrom", + new FieldDiff<>(from.allowMessagesFrom, to.allowMessagesFrom)); + } + if (from.allowConversationsFrom != to.allowConversationsFrom) { + addField("allowConversationsFrom", + new FieldDiff<>(from.allowConversationsFrom, to.allowConversationsFrom)); + } + if (from.suppressedVisualEffects != to.suppressedVisualEffects) { + addField("suppressedVisualEffects", + new FieldDiff<>(from.suppressedVisualEffects, to.suppressedVisualEffects)); + } + if (from.areChannelsBypassingDnd != to.areChannelsBypassingDnd) { + addField("areChannelsBypassingDnd", + new FieldDiff<>(from.areChannelsBypassingDnd, to.areChannelsBypassingDnd)); + } + + // Compare automatic and manual rules + final ArraySet<String> allRules = new ArraySet<>(); + addKeys(allRules, from.automaticRules); + addKeys(allRules, to.automaticRules); + final int num = allRules.size(); + for (int i = 0; i < num; i++) { + final String rule = allRules.valueAt(i); + final ZenModeConfig.ZenRule + fromRule = from.automaticRules != null ? from.automaticRules.get(rule) + : null; + final ZenModeConfig.ZenRule + toRule = to.automaticRules != null ? to.automaticRules.get(rule) : null; + RuleDiff ruleDiff = new RuleDiff(fromRule, toRule); + if (ruleDiff.hasDiff()) { + mAutomaticRulesDiff.put(rule, ruleDiff); + } + } + // If there's no diff this may turn out to be null, but that's also fine + RuleDiff manualRuleDiff = new RuleDiff(from.manualRule, to.manualRule); + if (manualRuleDiff.hasDiff()) { + mManualRuleDiff = manualRuleDiff; + } + } + + private static <T> void addKeys(ArraySet<T> set, ArrayMap<T, ?> map) { + if (map != null) { + for (int i = 0; i < map.size(); i++) { + set.add(map.keyAt(i)); + } + } + } + + /** + * Returns whether this diff object contains any diffs in any field. + */ + @Override + public boolean hasDiff() { + return hasExistenceChange() + || hasFieldDiffs() + || mManualRuleDiff != null + || mAutomaticRulesDiff.size() > 0; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Diff["); + if (!hasDiff()) { + sb.append("no changes"); + } + + // If added or deleted, then that's just the end of it + if (hasExistenceChange()) { + if (wasAdded()) { + sb.append("added"); + } else if (wasRemoved()) { + sb.append("removed"); + } + } + + // Handle top-level field change + boolean first = true; + for (String key : fieldNamesWithDiff()) { + FieldDiff diff = getDiffForField(key); + if (diff == null) { + // this shouldn't happen, but + continue; + } + if (first) { + first = false; + } else { + sb.append(",\n"); + } + + // Some special handling for people- and conversation-type fields for readability + if (PEOPLE_TYPE_FIELDS.contains(key)) { + sb.append(key); + sb.append(":"); + sb.append(ZenModeConfig.sourceToString((int) diff.from())); + sb.append("->"); + sb.append(ZenModeConfig.sourceToString((int) diff.to())); + } else if (key.equals(ALLOW_CONVERSATIONS_FROM_FIELD)) { + sb.append(key); + sb.append(":"); + sb.append(ZenPolicy.conversationTypeToString((int) diff.from())); + sb.append("->"); + sb.append(ZenPolicy.conversationTypeToString((int) diff.to())); + } else { + sb.append(key); + sb.append(":"); + sb.append(diff); + } + } + + // manual rule + if (mManualRuleDiff != null && mManualRuleDiff.hasDiff()) { + if (first) { + first = false; + } else { + sb.append(",\n"); + } + sb.append("manualRule:"); + sb.append(mManualRuleDiff); + } + + // automatic rules + for (String rule : mAutomaticRulesDiff.keySet()) { + RuleDiff diff = mAutomaticRulesDiff.get(rule); + if (diff != null && diff.hasDiff()) { + if (first) { + first = false; + } else { + sb.append(",\n"); + } + sb.append("automaticRule["); + sb.append(rule); + sb.append("]:"); + sb.append(diff); + } + } + + return sb.append(']').toString(); + } + + /** + * Get the diff in manual rule, if it exists. + */ + public RuleDiff getManualRuleDiff() { + return mManualRuleDiff; + } + + /** + * Get the full map of automatic rule diffs, or null if there are no diffs. + */ + public ArrayMap<String, RuleDiff> getAllAutomaticRuleDiffs() { + return (mAutomaticRulesDiff.size() > 0) ? mAutomaticRulesDiff : null; + } + } + + /** + * Diff class representing a change between two ZenRules. + */ + public static class RuleDiff extends BaseDiff { + /** + * Create a RuleDiff representing the difference between two ZenRule objects. + * @param from previous ZenRule + * @param to new ZenRule + * @return The diff between the two given ZenRules + */ + public RuleDiff(ZenModeConfig.ZenRule from, ZenModeConfig.ZenRule to) { + super(from, to); + // Short-circuit the both-null case + if (from == null && to == null) { + return; + } + // Return if the diff was added or removed + if (hasExistenceChange()) { + return; + } + + if (from.enabled != to.enabled) { + addField("enabled", new FieldDiff<>(from.enabled, to.enabled)); + } + if (from.snoozing != to.snoozing) { + addField("snoozing", new FieldDiff<>(from.snoozing, to.snoozing)); + } + if (!Objects.equals(from.name, to.name)) { + addField("name", new FieldDiff<>(from.name, to.name)); + } + if (from.zenMode != to.zenMode) { + addField("zenMode", new FieldDiff<>(from.zenMode, to.zenMode)); + } + if (!Objects.equals(from.conditionId, to.conditionId)) { + addField("conditionId", new FieldDiff<>(from.conditionId, to.conditionId)); + } + if (!Objects.equals(from.condition, to.condition)) { + addField("condition", new FieldDiff<>(from.condition, to.condition)); + } + if (!Objects.equals(from.component, to.component)) { + addField("component", new FieldDiff<>(from.component, to.component)); + } + if (!Objects.equals(from.configurationActivity, to.configurationActivity)) { + addField("configurationActivity", new FieldDiff<>( + from.configurationActivity, to.configurationActivity)); + } + if (!Objects.equals(from.id, to.id)) { + addField("id", new FieldDiff<>(from.id, to.id)); + } + if (from.creationTime != to.creationTime) { + addField("creationTime", + new FieldDiff<>(from.creationTime, to.creationTime)); + } + if (!Objects.equals(from.enabler, to.enabler)) { + addField("enabler", new FieldDiff<>(from.enabler, to.enabler)); + } + if (!Objects.equals(from.zenPolicy, to.zenPolicy)) { + addField("zenPolicy", new FieldDiff<>(from.zenPolicy, to.zenPolicy)); + } + if (from.modified != to.modified) { + addField("modified", new FieldDiff<>(from.modified, to.modified)); + } + if (!Objects.equals(from.pkg, to.pkg)) { + addField("pkg", new FieldDiff<>(from.pkg, to.pkg)); + } + } + + /** + * Returns whether this object represents an actual diff. + */ + @Override + public boolean hasDiff() { + return hasExistenceChange() || hasFieldDiffs(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ZenRuleDiff{"); + // If there's no diff, probably we haven't actually let this object continue existing + // but might as well handle this case. + if (!hasDiff()) { + sb.append("no changes"); + } + + // If added or deleted, then that's just the end of it + if (hasExistenceChange()) { + if (wasAdded()) { + sb.append("added"); + } else if (wasRemoved()) { + sb.append("removed"); + } + } + + // Go through all of the individual fields + boolean first = true; + for (String key : fieldNamesWithDiff()) { + FieldDiff diff = getDiffForField(key); + if (diff == null) { + // this shouldn't happen, but + continue; + } + if (first) { + first = false; + } else { + sb.append(", "); + } + + sb.append(key); + sb.append(":"); + sb.append(diff); + } + + return sb.append("}").toString(); + } + } +} diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index fcc64b088def..68cce4ae274a 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -50,6 +50,7 @@ import android.provider.Settings; import android.util.ArraySet; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IVoiceActionCheckCallback; import com.android.internal.app.IVoiceInteractionManagerService; @@ -200,6 +201,9 @@ public class VoiceInteractionService extends Service { private final Set<HotwordDetector> mActiveDetectors = new ArraySet<>(); + // True if any of the createAOHD methods should use the test ST module. + @GuardedBy("mLock") + private boolean mTestModuleForAlwaysOnHotwordDetectorEnabled = false; private void onDetectorRemoteException(@NonNull IBinder token, int detectorType) { Log.d(TAG, "onDetectorRemoteException for " + HotwordDetector.detectorTypeToString( @@ -512,14 +516,14 @@ public class VoiceInteractionService extends Service { Objects.requireNonNull(callback); return createAlwaysOnHotwordDetectorInternal(keyphrase, locale, /* supportHotwordDetectionService= */ false, /* options= */ null, - /* sharedMemory= */ null, /* moduleProperties */ null, executor, callback); + /* sharedMemory= */ null, /* moduleProperties= */ null, executor, callback); } /** * Same as {@link createAlwaysOnHotwordDetector(String, Locale, Executor, * AlwaysOnHotwordDetector.Callback)}, but allow explicit selection of the underlying ST * module to attach to. - * Use {@link listModuleProperties} to get available modules to attach to. + * Use {@link #listModuleProperties()} to get available modules to attach to. * @hide */ @TestApi @@ -645,14 +649,14 @@ public class VoiceInteractionService extends Service { Objects.requireNonNull(callback); return createAlwaysOnHotwordDetectorInternal(keyphrase, locale, /* supportHotwordDetectionService= */ true, options, sharedMemory, - /* moduleProperties */ null, executor, callback); + /* moduleProperties= */ null, executor, callback); } /** * Same as {@link createAlwaysOnHotwordDetector(String, Locale, * PersistableBundle, SharedMemory, Executor, AlwaysOnHotwordDetector.Callback)}, * but allow explicit selection of the underlying ST module to attach to. - * Use {@link listModuleProperties} to get available modules to attach to. + * Use {@link #listModuleProperties()} to get available modules to attach to. * @hide */ @TestApi @@ -717,6 +721,10 @@ public class VoiceInteractionService extends Service { try { dspDetector.registerOnDestroyListener(this::onHotwordDetectorDestroyed); + // Check if we are currently overridden, and should use the test module. + if (mTestModuleForAlwaysOnHotwordDetectorEnabled) { + moduleProperties = getTestModuleProperties(); + } // If moduleProperties is null, the default STModule is used. dspDetector.initialize(options, sharedMemory, moduleProperties); } catch (Exception e) { @@ -990,6 +998,44 @@ public class VoiceInteractionService extends Service { return mKeyphraseEnrollmentInfo; } + + /** + * Configure {@link createAlwaysOnHotwordDetector(String, Locale, + * SoundTrigger.ModuleProperties, Executor, AlwaysOnHotwordDetector.Callback)} + * and similar overloads to utilize the test SoundTrigger module instead of the + * actual DSP module. + * @param isEnabled - {@code true} if subsequently created {@link AlwaysOnHotwordDetector} + * objects should attach to a test module. {@code false} if subsequently created + * {@link AlwaysOnHotwordDetector} should attach to the actual DSP module. + * @hide + */ + @TestApi + public final void setTestModuleForAlwaysOnHotwordDetectorEnabled(boolean isEnabled) { + synchronized (mLock) { + mTestModuleForAlwaysOnHotwordDetectorEnabled = isEnabled; + } + } + + /** + * Get the {@link SoundTrigger.ModuleProperties} representing the fake + * STHAL to attach to via {@link createAlwaysOnHotwordDetector(String, Locale, + * SoundTrigger.ModuleProperties, Executor, AlwaysOnHotwordDetector.Callback)} and + * similar overloads for test purposes. + * @return ModuleProperties to use for test purposes. + */ + private final @NonNull SoundTrigger.ModuleProperties getTestModuleProperties() { + var moduleProps = listModuleProperties() + .stream() + .filter((SoundTrigger.ModuleProperties prop) + -> prop.getSupportedModelArch().equals(SoundTrigger.FAKE_HAL_ARCH)) + .findFirst() + .orElse(null); + if (moduleProps == null) { + throw new IllegalStateException("Fake ST HAL should always be available"); + } + return moduleProps; + } + /** * Checks if a given keyphrase and locale are supported to create an * {@link AlwaysOnHotwordDetector}. diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java index d35aff9a72b7..3812d37a5fed 100644 --- a/core/java/android/view/InputWindowHandle.java +++ b/core/java/android/view/InputWindowHandle.java @@ -215,6 +215,7 @@ public final class InputWindowHandle { .append(", scaleFactor=").append(scaleFactor) .append(", transform=").append(transform) .append(", windowToken=").append(windowToken) + .append(", displayId=").append(displayId) .append(", isClone=").append((inputConfig & InputConfig.CLONE) != 0) .toString(); diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index ac50d09cc091..cd89a561074c 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -99,9 +99,16 @@ public class SurfaceControlViewHost { @Override public ISurfaceSyncGroup getSurfaceSyncGroup() { CompletableFuture<ISurfaceSyncGroup> surfaceSyncGroup = new CompletableFuture<>(); - mViewRoot.mHandler.post( - () -> surfaceSyncGroup.complete( - mViewRoot.getOrCreateSurfaceSyncGroup().mISurfaceSyncGroup)); + // If the call came from in process and it's already running on the UI thread, return + // results immediately instead of posting to the main thread. If we post to the main + // thread, it will block itself and the return value will always be null. + if (Thread.currentThread() == mViewRoot.mThread) { + return mViewRoot.getOrCreateSurfaceSyncGroup().mISurfaceSyncGroup; + } else { + mViewRoot.mHandler.post( + () -> surfaceSyncGroup.complete( + mViewRoot.getOrCreateSurfaceSyncGroup().mISurfaceSyncGroup)); + } try { return surfaceSyncGroup.get(1, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 2f5cd5434b89..055b5cb70562 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6946,6 +6946,7 @@ public final class ViewRootImpl implements ViewParent, return; } final boolean needsStylusPointerIcon = event.isStylusPointer() + && event.isHoverEvent() && mInputManager.isStylusPointerIconEnabled(); if (needsStylusPointerIcon || event.isFromSource(InputDevice.SOURCE_MOUSE)) { if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 088065d2f77d..7931d1a06d39 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -8095,12 +8095,14 @@ public class Editor { private boolean mIsInsertModeActive; private InsertModeTransformationMethod mInsertModeTransformationMethod; private final Paint mHighlightPaint; + private final Path mHighlightPath; InsertModeController(@NonNull TextView textView) { mTextView = Objects.requireNonNull(textView); mIsInsertModeActive = false; mInsertModeTransformationMethod = null; mHighlightPaint = new Paint(); + mHighlightPath = new Path(); // The highlight color is supposed to be 12% of the color primary40. We can't // directly access Material 3 theme. But because Material 3 sets the colorPrimary to @@ -8168,10 +8170,8 @@ public class Editor { ((InsertModeTransformationMethod.TransformedText) transformedText); final int highlightStart = insertModeTransformedText.getHighlightStart(); final int highlightEnd = insertModeTransformedText.getHighlightEnd(); - final Layout.SelectionRectangleConsumer consumer = - (left, top, right, bottom, textSelectionLayout) -> - canvas.drawRect(left, top, right, bottom, mHighlightPaint); - layout.getSelection(highlightStart, highlightEnd, consumer); + layout.getSelectionPath(highlightStart, highlightEnd, mHighlightPath); + canvas.drawPath(mHighlightPath, mHighlightPaint); } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 18874f768929..3165654d806d 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -1780,6 +1780,21 @@ public class RemoteViews implements Parcelable, Filter { Object value = getParameterValue(view); try { MethodHandle method = getMethod(view, this.methodName, param, true /* async */); + // Upload the bitmap to GPU if the parameter is of type Bitmap or Icon. + // Since bitmaps in framework are seldomly modified, this is supposed to accelerate + // the operations. + if (value instanceof Bitmap bitmap) { + bitmap.prepareToDraw(); + } + + if (value instanceof Icon icon + && (icon.getType() == Icon.TYPE_BITMAP + || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) { + Bitmap bitmap = icon.getBitmap(); + if (bitmap != null) { + bitmap.prepareToDraw(); + } + } if (method != null) { Runnable endAction = (Runnable) method.invoke(view, value); diff --git a/core/java/com/android/internal/app/ISoundTriggerService.aidl b/core/java/com/android/internal/app/ISoundTriggerService.aidl index ab7f602e2dfc..ed751cb481c5 100644 --- a/core/java/com/android/internal/app/ISoundTriggerService.aidl +++ b/core/java/com/android/internal/app/ISoundTriggerService.aidl @@ -16,8 +16,9 @@ package com.android.internal.app; -import android.media.permission.Identity; import android.hardware.soundtrigger.SoundTrigger; +import android.media.permission.Identity; +import android.media.soundtrigger_middleware.ISoundTriggerInjection; import com.android.internal.app.ISoundTriggerSession; /** @@ -74,4 +75,8 @@ interface ISoundTriggerService { */ List<SoundTrigger.ModuleProperties> listModuleProperties(in Identity originatorIdentity); + /** + * Attach an HAL injection interface. + */ + void attachInjection(ISoundTriggerInjection injection); } diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl index 6b40d9873fbb..24d5afc42d8f 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -96,6 +96,21 @@ interface IVoiceInteractionManagerService { * @RequiresPermission Manifest.permission.MANAGE_VOICE_KEYPHRASES */ int deleteKeyphraseSoundModel(int keyphraseId, in String bcp47Locale); + + /** + * Override the persistent enrolled model database with an in-memory + * fake for testing purposes. + * + * @param enabled - {@code true} to enable the test database. {@code false} to enable + * the real, persistent database. + * @param token - IBinder used to register a death listener to clean-up the override + * if tests do not clean up gracefully. + */ + @EnforcePermission("MANAGE_VOICE_KEYPHRASES") + @JavaPassthrough(annotation= "@android.annotation.RequiresPermission(" + + "android.Manifest.permission.MANAGE_VOICE_KEYPHRASES)") + void setModelDatabaseForTestEnabled(boolean enabled, IBinder token); + /** * Indicates if there's a keyphrase sound model available for the given keyphrase ID and the * user ID of the caller. @@ -106,6 +121,7 @@ interface IVoiceInteractionManagerService { * @param bcp47Locale The BCP47 language tag for the keyphrase's locale. */ boolean isEnrolledForKeyphrase(int keyphraseId, String bcp47Locale); + /** * Generates KeyphraseMetadata for an enrolled sound model based on keyphrase string, locale, * and the user ID of the caller. diff --git a/core/java/com/android/internal/util/DumpUtils.java b/core/java/com/android/internal/util/DumpUtils.java index f6d80a572c75..8fe2b9cdf1e5 100644 --- a/core/java/com/android/internal/util/DumpUtils.java +++ b/core/java/com/android/internal/util/DumpUtils.java @@ -25,6 +25,7 @@ import android.os.Binder; import android.os.Handler; import android.text.TextUtils; import android.util.Slog; +import android.util.SparseArray; import java.io.PrintWriter; import java.io.StringWriter; @@ -312,5 +313,85 @@ public final class DumpUtils { || cn.flattenToString().toLowerCase().contains(filterString.toLowerCase()); }; } -} + /** + * Lambda used to dump a key (and its index) while iterating though a collection. + */ + public interface KeyDumper { + + /** Dumps the index and key.*/ + void dump(int index, int key); + } + + /** + * Lambda used to dump a value while iterating though a collection. + * + * @param <T> type of the value. + */ + public interface ValueDumper<T> { + + /** Dumps the value.*/ + void dump(T value); + } + + /** + * Dumps a sparse array. + */ + public static void dumpSparseArray(PrintWriter pw, String prefix, SparseArray<?> array, + String name) { + dumpSparseArray(pw, prefix, array, name, /* keyDumper= */ null, /* valueDumper= */ null); + } + + /** + * Dumps the values of a sparse array. + */ + public static <T> void dumpSparseArrayValues(PrintWriter pw, String prefix, + SparseArray<T> array, String name) { + dumpSparseArray(pw, prefix, array, name, (i, k) -> { + pw.printf("%s%s", prefix, prefix); + }, /* valueDumper= */ null); + } + + /** + * Dumps a sparse array, customizing each line. + */ + public static <T> void dumpSparseArray(PrintWriter pw, String prefix, SparseArray<T> array, + String name, @Nullable KeyDumper keyDumper, @Nullable ValueDumper<T> valueDumper) { + int size = array.size(); + if (size == 0) { + pw.print(prefix); + pw.print("No "); + pw.print(name); + pw.println("s"); + return; + } + pw.print(prefix); + pw.print(size); + pw.print(' '); + pw.print(name); + pw.println("(s):"); + + String prefix2 = prefix + prefix; + for (int i = 0; i < size; i++) { + int key = array.keyAt(i); + T value = array.valueAt(i); + if (keyDumper != null) { + keyDumper.dump(i, key); + } else { + pw.print(prefix2); + pw.print(i); + pw.print(": "); + pw.print(key); + pw.print("->"); + } + if (value == null) { + pw.print("(null)"); + } else if (valueDumper != null) { + valueDumper.dump(value); + } else { + pw.print(value); + } + pw.println(); + } + } +} diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java b/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java index 316c70c45fb4..c6bb07b17fd4 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java @@ -26,7 +26,7 @@ import android.app.Activity; import android.compat.testing.PlatformCompatChangeRule; import android.os.Bundle; import android.platform.test.annotations.IwTest; -import android.platform.test.annotations.Postsubmit; +import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.util.PollingCheck; import android.view.View; @@ -60,7 +60,7 @@ import java.util.concurrent.atomic.AtomicReference; */ @RunWith(AndroidJUnit4.class) @LargeTest -@Postsubmit +@Presubmit public class FontScaleConverterActivityTest { @Rule public ActivityScenarioRule<TestActivity> rule = new ActivityScenarioRule<>(TestActivity.class); diff --git a/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java b/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java new file mode 100644 index 000000000000..66f3bca72aeb --- /dev/null +++ b/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.biometrics; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.RemoteException; +import android.os.test.TestLooper; +import android.platform.test.annotations.Presubmit; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.MockitoRule; + +import java.util.concurrent.Executor; + + +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class BiometricPromptTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private Context mContext; + @Mock + private IAuthService mService; + private BiometricPrompt mBiometricPrompt; + + private CancellationSignal mCancellationSignal; + + private final TestLooper mLooper = new TestLooper(); + private final Handler mHandler = new Handler(mLooper.getLooper()); + private final Executor mExecutor = mHandler::post; + + @Before + public void setUp() throws RemoteException { + mBiometricPrompt = new BiometricPrompt.Builder(mContext) + .setUseDefaultSubtitle() + .setUseDefaultTitle() + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG + | BiometricManager.Authenticators.DEVICE_CREDENTIAL) + .setService(mService) + .build(); + + mCancellationSignal = new CancellationSignal(); + when(mService.authenticate(any(), anyLong(), anyInt(), any(), anyString(), any())) + .thenReturn(0L); + when(mContext.getPackageName()).thenReturn("BiometricPromptTest"); + } + + @Test + public void testCancellationAfterAuthenticationFailed() throws RemoteException { + ArgumentCaptor<IBiometricServiceReceiver> biometricServiceReceiverCaptor = + ArgumentCaptor.forClass(IBiometricServiceReceiver.class); + BiometricPrompt.AuthenticationCallback callback = + new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + }}; + mBiometricPrompt.authenticate(mCancellationSignal, mExecutor, callback); + mLooper.dispatchAll(); + + verify(mService).authenticate(any(), anyLong(), anyInt(), + biometricServiceReceiverCaptor.capture(), anyString(), any()); + + biometricServiceReceiverCaptor.getValue().onAuthenticationFailed(); + mLooper.dispatchAll(); + mCancellationSignal.cancel(); + + verify(mService).cancelAuthentication(any(), anyString(), anyLong()); + } +} diff --git a/core/tests/coretests/src/android/hardware/biometrics/OWNERS b/core/tests/coretests/src/android/hardware/biometrics/OWNERS new file mode 100644 index 000000000000..6a2192a2c7fb --- /dev/null +++ b/core/tests/coretests/src/android/hardware/biometrics/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/biometrics/OWNERS diff --git a/core/tests/coretests/src/com/android/internal/util/DumpUtilsTest.java b/core/tests/coretests/src/com/android/internal/util/DumpUtilsTest.java index 4716312c59a8..36c2a62ae6ed 100644 --- a/core/tests/coretests/src/com/android/internal/util/DumpUtilsTest.java +++ b/core/tests/coretests/src/com/android/internal/util/DumpUtilsTest.java @@ -23,16 +23,25 @@ import static com.android.internal.util.DumpUtils.isPlatformCriticalPackage; import static com.android.internal.util.DumpUtils.isPlatformNonCriticalPackage; import static com.android.internal.util.DumpUtils.isPlatformPackage; +import static com.google.common.truth.Truth.assertWithMessage; + import android.content.ComponentName; +import android.util.SparseArray; import junit.framework.TestCase; +import java.io.PrintWriter; +import java.io.StringWriter; + /** * Run with: atest FrameworksCoreTests:DumpUtilsTest */ public class DumpUtilsTest extends TestCase { + private final StringWriter mStringWriter = new StringWriter(); + private final PrintWriter mPrintWriter = new PrintWriter(mStringWriter); + private static ComponentName cn(String componentName) { if (componentName == null) { return null; @@ -168,4 +177,144 @@ public class DumpUtilsTest extends TestCase { Integer.toHexString(System.identityHashCode(component))).test( wcn("com.google/.abc"))); } + + public void testDumpSparseArray_empty() { + SparseArray<String> array = new SparseArray<>(); + + DumpUtils.dumpSparseArray(mPrintWriter, /* prefix= */ "...", array, "whatever"); + + String output = flushPrintWriter(); + + assertWithMessage("empty array dump").that(output).isEqualTo("...No whatevers\n"); + } + + public void testDumpSparseArray_oneElement() { + SparseArray<String> array = new SparseArray<>(); + array.put(1, "uno"); + + DumpUtils.dumpSparseArray(mPrintWriter, /* prefix= */ ".", array, "number"); + + String output = flushPrintWriter(); + + assertWithMessage("dump of %s", array).that(output).isEqualTo("" + + ".1 number(s):\n" + + "..0: 1->uno\n"); + } + + public void testDumpSparseArray_oneNullElement() { + SparseArray<String> array = new SparseArray<>(); + array.put(1, null); + + DumpUtils.dumpSparseArray(mPrintWriter, /* prefix= */ ".", array, "NULL"); + + String output = flushPrintWriter(); + + assertWithMessage("dump of %s", array).that(output).isEqualTo("" + + ".1 NULL(s):\n" + + "..0: 1->(null)\n"); + } + + public void testDumpSparseArray_multipleElements() { + SparseArray<String> array = new SparseArray<>(); + array.put(1, "uno"); + array.put(2, "duo"); + array.put(42, null); + + DumpUtils.dumpSparseArray(mPrintWriter, /* prefix= */ ".", array, "number"); + + String output = flushPrintWriter(); + + assertWithMessage("dump of %s", array).that(output).isEqualTo("" + + ".3 number(s):\n" + + "..0: 1->uno\n" + + "..1: 2->duo\n" + + "..2: 42->(null)\n"); + } + + public void testDumpSparseArray_keyDumperOnly() { + SparseArray<String> array = new SparseArray<>(); + array.put(1, "uno"); + array.put(2, "duo"); + array.put(42, null); + + DumpUtils.dumpSparseArray(mPrintWriter, /* prefix= */ ".", array, "number", + (i, k) -> { + mPrintWriter.printf("_%d=%d_", i, k); + }, /* valueDumper= */ null); + + String output = flushPrintWriter(); + + assertWithMessage("dump of %s", array).that(output).isEqualTo("" + + ".3 number(s):\n" + + "_0=1_uno\n" + + "_1=2_duo\n" + + "_2=42_(null)\n"); + } + + public void testDumpSparseArray_valueDumperOnly() { + SparseArray<String> array = new SparseArray<>(); + array.put(1, "uno"); + array.put(2, "duo"); + array.put(42, null); + + DumpUtils.dumpSparseArray(mPrintWriter, /* prefix= */ ".", array, "number", + /* keyDumper= */ null, + s -> { + mPrintWriter.print(s.toUpperCase()); + }); + + String output = flushPrintWriter(); + + assertWithMessage("dump of %s", array).that(output).isEqualTo("" + + ".3 number(s):\n" + + "..0: 1->UNO\n" + + "..1: 2->DUO\n" + + "..2: 42->(null)\n"); + } + + public void testDumpSparseArray_keyAndValueDumpers() { + SparseArray<String> array = new SparseArray<>(); + array.put(1, "uno"); + array.put(2, "duo"); + array.put(42, null); + + DumpUtils.dumpSparseArray(mPrintWriter, /* prefix= */ ".", array, "number", + (i, k) -> { + mPrintWriter.printf("_%d=%d_", i, k); + }, + s -> { + mPrintWriter.print(s.toUpperCase()); + }); + + String output = flushPrintWriter(); + + assertWithMessage("dump of %s", array).that(output).isEqualTo("" + + ".3 number(s):\n" + + "_0=1_UNO\n" + + "_1=2_DUO\n" + + "_2=42_(null)\n"); + } + + public void testDumpSparseArrayValues() { + SparseArray<String> array = new SparseArray<>(); + array.put(1, "uno"); + array.put(2, "duo"); + array.put(42, null); + + DumpUtils.dumpSparseArrayValues(mPrintWriter, /* prefix= */ ".", array, "number"); + + String output = flushPrintWriter(); + + assertWithMessage("dump of %s", array).that(output).isEqualTo("" + + ".3 numbers:\n" + + "..uno\n" + + "..duo\n" + + "..(null)\n"); + } + + private String flushPrintWriter() { + mPrintWriter.flush(); + + return mStringWriter.toString(); + } } diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 25b074d20b81..2307d6080f9f 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -401,8 +401,9 @@ public final class Bitmap implements Parcelable { /** * This is called by methods that want to throw an exception if the bitmap * has already been recycled. + * @hide */ - private void checkRecycled(String errorMessage) { + void checkRecycled(String errorMessage) { if (mRecycled) { throw new IllegalStateException(errorMessage); } diff --git a/graphics/java/android/graphics/BitmapShader.java b/graphics/java/android/graphics/BitmapShader.java index 2f6dd468511b..5c065775eea2 100644 --- a/graphics/java/android/graphics/BitmapShader.java +++ b/graphics/java/android/graphics/BitmapShader.java @@ -120,6 +120,7 @@ public class BitmapShader extends Shader { if (bitmap == null) { throw new IllegalArgumentException("Bitmap must be non-null"); } + bitmap.checkRecycled("Cannot create BitmapShader for recycled bitmap"); mBitmap = bitmap; mTileX = tileX; mTileY = tileY; @@ -188,6 +189,8 @@ public class BitmapShader extends Shader { /** @hide */ @Override protected long createNativeInstance(long nativeMatrix, boolean filterFromPaint) { + mBitmap.checkRecycled("BitmapShader's bitmap has been recycled"); + boolean enableLinearFilter = mFilterMode == FILTER_MODE_LINEAR; if (mFilterMode == FILTER_MODE_DEFAULT) { mFilterFromPaint = filterFromPaint; diff --git a/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java b/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java index d785c3c895b8..f26b50ed4e2a 100644 --- a/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java +++ b/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java @@ -21,10 +21,7 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; -import android.os.RemoteException; import android.os.ServiceManager; -import android.security.GenerateRkpKey; -import android.security.keymaster.KeymasterDefs; class CredstoreIdentityCredentialStore extends IdentityCredentialStore { @@ -125,18 +122,7 @@ class CredstoreIdentityCredentialStore extends IdentityCredentialStore { @NonNull String docType) throws AlreadyPersonalizedException, DocTypeNotSupportedException { try { - IWritableCredential wc; - wc = mStore.createCredential(credentialName, docType); - try { - GenerateRkpKey keyGen = new GenerateRkpKey(mContext); - // We don't know what the security level is for the backing keymint, so go ahead and - // poke the provisioner for both TEE and SB. - keyGen.notifyKeyGenerated(KeymasterDefs.KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT); - keyGen.notifyKeyGenerated(KeymasterDefs.KM_SECURITY_LEVEL_STRONGBOX); - } catch (RemoteException e) { - // Not really an error state. Does not apply at all if RKP is unsupported or - // disabled on a given device. - } + IWritableCredential wc = mStore.createCredential(credentialName, docType); return new CredstoreWritableIdentityCredential(mContext, credentialName, docType, wc); } catch (android.os.RemoteException e) { throw new RuntimeException("Unexpected RemoteException ", e); diff --git a/keystore/java/android/security/GenerateRkpKey.java b/keystore/java/android/security/GenerateRkpKey.java deleted file mode 100644 index 698133287f63..000000000000 --- a/keystore/java/android/security/GenerateRkpKey.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2021 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.security; - -import android.annotation.CheckResult; -import android.annotation.IntDef; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -/** - * GenerateKey is a helper class to handle interactions between Keystore and the RemoteProvisioner - * app. There are two cases where Keystore should use this class. - * - * (1) : An app generates a new attested key pair, so Keystore calls notifyKeyGenerated to let the - * RemoteProvisioner app check if the state of the attestation key pool is getting low enough - * to warrant provisioning more attestation certificates early. - * - * (2) : An app attempts to generate a new key pair, but the keystore service discovers it is out of - * attestation key pairs and cannot provide one for the given application. Keystore can then - * make a blocking call on notifyEmpty to allow the RemoteProvisioner app to get another - * attestation certificate chain provisioned. - * - * In most cases, the proper usage of (1) should preclude the need for (2). - * - * @hide - */ -public class GenerateRkpKey { - private static final String TAG = "GenerateRkpKey"; - - private static final int NOTIFY_EMPTY = 0; - private static final int NOTIFY_KEY_GENERATED = 1; - private static final int TIMEOUT_MS = 1000; - - private IGenerateRkpKeyService mBinder; - private Context mContext; - private CountDownLatch mCountDownLatch; - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = { - IGenerateRkpKeyService.Status.OK, - IGenerateRkpKeyService.Status.NO_NETWORK_CONNECTIVITY, - IGenerateRkpKeyService.Status.NETWORK_COMMUNICATION_ERROR, - IGenerateRkpKeyService.Status.DEVICE_NOT_REGISTERED, - IGenerateRkpKeyService.Status.HTTP_CLIENT_ERROR, - IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR, - IGenerateRkpKeyService.Status.HTTP_UNKNOWN_ERROR, - IGenerateRkpKeyService.Status.INTERNAL_ERROR, - }) - public @interface Status { - } - - private ServiceConnection mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - mBinder = IGenerateRkpKeyService.Stub.asInterface(service); - mCountDownLatch.countDown(); - } - - @Override public void onBindingDied(ComponentName className) { - mCountDownLatch.countDown(); - } - - @Override - public void onServiceDisconnected(ComponentName className) { - mBinder = null; - } - }; - - /** - * Constructor which takes a Context object. - */ - public GenerateRkpKey(Context context) { - mContext = context; - } - - @Status - private int bindAndSendCommand(int command, int securityLevel) throws RemoteException { - Intent intent = new Intent(IGenerateRkpKeyService.class.getName()); - ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); - int returnCode = IGenerateRkpKeyService.Status.OK; - if (comp == null) { - // On a system that does not use RKP, the RemoteProvisioner app won't be installed. - return returnCode; - } - intent.setComponent(comp); - mCountDownLatch = new CountDownLatch(1); - Executor executor = Executors.newCachedThreadPool(); - if (!mContext.bindService(intent, Context.BIND_AUTO_CREATE, executor, mConnection)) { - throw new RemoteException("Failed to bind to GenerateRkpKeyService"); - } - try { - mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Log.e(TAG, "Interrupted: ", e); - } - if (mBinder != null) { - switch (command) { - case NOTIFY_EMPTY: - returnCode = mBinder.generateKey(securityLevel); - break; - case NOTIFY_KEY_GENERATED: - mBinder.notifyKeyGenerated(securityLevel); - break; - default: - Log.e(TAG, "Invalid case for command"); - } - } else { - Log.e(TAG, "Binder object is null; failed to bind to GenerateRkpKeyService."); - returnCode = IGenerateRkpKeyService.Status.INTERNAL_ERROR; - } - mContext.unbindService(mConnection); - return returnCode; - } - - /** - * Fulfills the use case of (2) described in the class documentation. Blocks until the - * RemoteProvisioner application can get new attestation keys signed by the server. - * @return the status of the key generation - */ - @CheckResult - @Status - public int notifyEmpty(int securityLevel) throws RemoteException { - return bindAndSendCommand(NOTIFY_EMPTY, securityLevel); - } - - /** - * Fulfills the use case of (1) described in the class documentation. Non blocking call. - */ - public void notifyKeyGenerated(int securityLevel) throws RemoteException { - bindAndSendCommand(NOTIFY_KEY_GENERATED, securityLevel); - } -} diff --git a/keystore/java/android/security/IGenerateRkpKeyService.aidl b/keystore/java/android/security/IGenerateRkpKeyService.aidl deleted file mode 100644 index eeaeb27a7c77..000000000000 --- a/keystore/java/android/security/IGenerateRkpKeyService.aidl +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (C) 2021 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.security; - -/** - * Interface to allow the framework to notify the RemoteProvisioner app when keys are empty. This - * will be used if Keystore replies with an error code NO_KEYS_AVAILABLE in response to an - * attestation request. The framework can then synchronously call generateKey() to get more - * attestation keys generated and signed. Upon return, the caller can be certain an attestation key - * is available. - * - * @hide - */ -interface IGenerateRkpKeyService { - @JavaDerive(toString=true) - @Backing(type="int") - enum Status { - /** No error(s) occurred */ - OK = 0, - /** Unable to provision keys due to a lack of internet connectivity. */ - NO_NETWORK_CONNECTIVITY = 1, - /** An error occurred while communicating with the RKP server. */ - NETWORK_COMMUNICATION_ERROR = 2, - /** The given device was not registered with the RKP backend. */ - DEVICE_NOT_REGISTERED = 4, - /** The RKP server returned an HTTP client error, indicating a misbehaving client. */ - HTTP_CLIENT_ERROR = 5, - /** The RKP server returned an HTTP server error, indicating something went wrong on the server. */ - HTTP_SERVER_ERROR = 6, - /** The RKP server returned an HTTP status that is unknown. This should never happen. */ - HTTP_UNKNOWN_ERROR = 7, - /** An unexpected internal error occurred. This should never happen. */ - INTERNAL_ERROR = 8, - } - - /** - * Ping the provisioner service to let it know an app generated a key. This may or may not have - * consumed a remotely provisioned attestation key, so the RemoteProvisioner app should check. - */ - oneway void notifyKeyGenerated(in int securityLevel); - - /** - * Ping the provisioner service to indicate there are no remaining attestation keys left. - */ - Status generateKey(in int securityLevel); -} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java index c3b0f9bc16d3..474b7ea56be9 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -20,7 +20,6 @@ import static android.security.keystore2.AndroidKeyStoreCipherSpiBase.DEFAULT_MG import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityThread; import android.content.Context; import android.hardware.security.keymint.EcCurve; import android.hardware.security.keymint.KeyParameter; @@ -28,9 +27,6 @@ import android.hardware.security.keymint.KeyPurpose; import android.hardware.security.keymint.SecurityLevel; import android.hardware.security.keymint.Tag; import android.os.Build; -import android.os.RemoteException; -import android.security.GenerateRkpKey; -import android.security.IGenerateRkpKeyService; import android.security.KeyPairGeneratorSpec; import android.security.KeyStore2; import android.security.KeyStoreException; @@ -621,45 +617,6 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato @Override public KeyPair generateKeyPair() { - GenerateKeyPairHelperResult result = new GenerateKeyPairHelperResult(0, null); - for (int i = 0; i < 2; i++) { - /** - * NOTE: There is no need to delay between re-tries because the call to - * GenerateRkpKey.notifyEmpty() will delay for a while before returning. - */ - result = generateKeyPairHelper(); - if (result.rkpStatus == KeyStoreException.RKP_SUCCESS && result.keyPair != null) { - return result.keyPair; - } - } - - // RKP failure - if (result.rkpStatus != KeyStoreException.RKP_SUCCESS) { - KeyStoreException ksException = new KeyStoreException(ResponseCode.OUT_OF_KEYS, - "Could not get RKP keys", result.rkpStatus); - throw new ProviderException("Failed to provision new attestation keys.", ksException); - } - - return result.keyPair; - } - - private static class GenerateKeyPairHelperResult { - // Zero indicates success, non-zero indicates failure. Values should be - // {@link android.security.KeyStoreException#RKP_TEMPORARILY_UNAVAILABLE}, - // {@link android.security.KeyStoreException#RKP_SERVER_REFUSED_ISSUANCE}, - // {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_CONNECTIVITY} - // {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_SOFTWARE_REBOOT} - public final int rkpStatus; - @Nullable - public final KeyPair keyPair; - - private GenerateKeyPairHelperResult(int rkpStatus, KeyPair keyPair) { - this.rkpStatus = rkpStatus; - this.keyPair = keyPair; - } - } - - private GenerateKeyPairHelperResult generateKeyPairHelper() { if (mKeyStore == null || mSpec == null) { throw new IllegalStateException("Not initialized"); } @@ -697,26 +654,12 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato AndroidKeyStorePublicKey publicKey = AndroidKeyStoreProvider.makeAndroidKeyStorePublicKeyFromKeyEntryResponse( descriptor, metadata, iSecurityLevel, mKeymasterAlgorithm); - GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread - .currentApplication()); - try { - if (mSpec.getAttestationChallenge() != null) { - keyGen.notifyKeyGenerated(securityLevel); - } - } catch (RemoteException e) { - // This is not really an error state, and necessarily does not apply to non RKP - // systems or hybrid systems where RKP is not currently turned on. - Log.d(TAG, "Couldn't connect to the RemoteProvisioner backend.", e); - } success = true; - KeyPair kp = new KeyPair(publicKey, publicKey.getPrivateKey()); - return new GenerateKeyPairHelperResult(0, kp); + return new KeyPair(publicKey, publicKey.getPrivateKey()); } catch (KeyStoreException e) { switch (e.getErrorCode()) { case KeymasterDefs.KM_ERROR_HARDWARE_TYPE_UNAVAILABLE: throw new StrongBoxUnavailableException("Failed to generated key pair.", e); - case ResponseCode.OUT_OF_KEYS: - return checkIfRetryableOrThrow(e, securityLevel); default: ProviderException p = new ProviderException("Failed to generate key pair.", e); if ((mSpec.getPurposes() & KeyProperties.PURPOSE_WRAP_KEY) != 0) { @@ -742,55 +685,6 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato } } - // In case keystore reports OUT_OF_KEYS, call this handler in an attempt to remotely provision - // some keys. - GenerateKeyPairHelperResult checkIfRetryableOrThrow(KeyStoreException e, int securityLevel) { - GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread - .currentApplication()); - KeyStoreException ksException; - try { - final int keyGenStatus = keyGen.notifyEmpty(securityLevel); - // Default stance: temporary error. This is a hint to the caller to try again with - // exponential back-off. - int rkpStatus; - switch (keyGenStatus) { - case IGenerateRkpKeyService.Status.NO_NETWORK_CONNECTIVITY: - rkpStatus = KeyStoreException.RKP_FETCHING_PENDING_CONNECTIVITY; - break; - case IGenerateRkpKeyService.Status.DEVICE_NOT_REGISTERED: - rkpStatus = KeyStoreException.RKP_SERVER_REFUSED_ISSUANCE; - break; - case IGenerateRkpKeyService.Status.OK: - // Explicitly return not-OK here so we retry in generateKeyPair. All other cases - // should throw because a retry doesn't make sense if we didn't actually - // provision fresh keys. - return new GenerateKeyPairHelperResult( - KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE, null); - case IGenerateRkpKeyService.Status.NETWORK_COMMUNICATION_ERROR: - case IGenerateRkpKeyService.Status.HTTP_CLIENT_ERROR: - case IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR: - case IGenerateRkpKeyService.Status.HTTP_UNKNOWN_ERROR: - case IGenerateRkpKeyService.Status.INTERNAL_ERROR: - default: - // These errors really should never happen. The best we can do is assume they - // are transient and hint to the caller to retry with back-off. - rkpStatus = KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE; - break; - } - ksException = new KeyStoreException( - ResponseCode.OUT_OF_KEYS, - "Out of RKP keys due to IGenerateRkpKeyService status: " + keyGenStatus, - rkpStatus); - } catch (RemoteException f) { - ksException = new KeyStoreException( - ResponseCode.OUT_OF_KEYS, - "Remote exception: " + f.getMessage(), - KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE); - } - ksException.initCause(e); - throw new ProviderException("Failed to provision new attestation keys.", ksException); - } - private void addAttestationParameters(@NonNull List<KeyParameter> params) throws ProviderException, IllegalArgumentException, DeviceIdAttestationException { byte[] challenge = mSpec.getAttestationChallenge(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index dd91a37039e4..04cb17c6a735 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -690,6 +690,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (options1 == null) options1 = new Bundle(); if (taskId2 == INVALID_TASK_ID) { // Launching a solo task. + // Exit split first if this task under split roots. + if (mMainStage.containsTask(taskId1) || mSideStage.containsTask(taskId1)) { + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); + } ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); options1 = activityOptions.toBundle(); 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 060dc4e05b46..dfde7e6feff5 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 @@ -110,19 +110,11 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - final int outsetLeftId = R.dimen.freeform_resize_handle; - final int outsetTopId = R.dimen.freeform_resize_handle; - final int outsetRightId = R.dimen.freeform_resize_handle; - final int outsetBottomId = R.dimen.freeform_resize_handle; - mRelayoutParams.reset(); mRelayoutParams.mRunningTaskInfo = taskInfo; mRelayoutParams.mLayoutResId = R.layout.caption_window_decor; mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; mRelayoutParams.mShadowRadiusId = shadowRadiusID; - if (isDragResizeable) { - mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); - } relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index f9c0e600dd38..a004e37c6345 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 @@ -208,11 +208,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - final int outsetLeftId = R.dimen.freeform_resize_handle; - final int outsetTopId = R.dimen.freeform_resize_handle; - final int outsetRightId = R.dimen.freeform_resize_handle; - final int outsetBottomId = R.dimen.freeform_resize_handle; - final int windowDecorLayoutId = getDesktopModeWindowDecorLayoutId( taskInfo.getWindowingMode()); mRelayoutParams.reset(); @@ -220,9 +215,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mRelayoutParams.mLayoutResId = windowDecorLayoutId; mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; mRelayoutParams.mShadowRadiusId = shadowRadiusID; - if (isDragResizeable) { - mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); - } relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo @@ -424,13 +416,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_controls_window_decor) { // Align the handle menu to the left of the caption. - menuX = mRelayoutParams.mCaptionX - mResult.mDecorContainerOffsetX + mMarginMenuStart; - menuY = mRelayoutParams.mCaptionY - mResult.mDecorContainerOffsetY + mMarginMenuTop; + menuX = mRelayoutParams.mCaptionX + mMarginMenuStart; + menuY = mRelayoutParams.mCaptionY + mMarginMenuTop; } else { // Position the handle menu at the center of the caption. - menuX = mRelayoutParams.mCaptionX + (captionWidth / 2) - (mMenuWidth / 2) - - mResult.mDecorContainerOffsetX; - menuY = mRelayoutParams.mCaptionY - mResult.mDecorContainerOffsetY + mMarginMenuStart; + menuX = mRelayoutParams.mCaptionX + (captionWidth / 2) - (mMenuWidth / 2); + menuY = mRelayoutParams.mCaptionY + mMarginMenuStart; } // App Info pill setup. @@ -497,23 +488,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final boolean pointInAppInfoPill = pointInView( mHandleMenuAppInfoPill.mWindowViewHost.getView(), - inputPoint.x - mHandleMenuAppInfoPillPosition.x - mResult.mDecorContainerOffsetX, - inputPoint.y - mHandleMenuAppInfoPillPosition.y - - mResult.mDecorContainerOffsetY); + inputPoint.x - mHandleMenuAppInfoPillPosition.x, + inputPoint.y - mHandleMenuAppInfoPillPosition.y); boolean pointInWindowingPill = false; if (mHandleMenuWindowingPill != null) { pointInWindowingPill = pointInView(mHandleMenuWindowingPill.mWindowViewHost.getView(), - inputPoint.x - mHandleMenuWindowingPillPosition.x - - mResult.mDecorContainerOffsetX, - inputPoint.y - mHandleMenuWindowingPillPosition.y - - mResult.mDecorContainerOffsetY); + inputPoint.x - mHandleMenuWindowingPillPosition.x, + inputPoint.y - mHandleMenuWindowingPillPosition.y); } final boolean pointInMoreActionsPill = pointInView( mHandleMenuMoreActionsPill.mWindowViewHost.getView(), - inputPoint.x - mHandleMenuMoreActionsPillPosition.x - - mResult.mDecorContainerOffsetX, - inputPoint.y - mHandleMenuMoreActionsPillPosition.y - - mResult.mDecorContainerOffsetY); + inputPoint.x - mHandleMenuMoreActionsPillPosition.x, + inputPoint.y - mHandleMenuMoreActionsPillPosition.y); if (!pointInAppInfoPill && !pointInWindowingPill && !pointInMoreActionsPill && !pointInOpenMenuButton) { closeHandleMenu(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 8cb575cc96e3..d5437c72acac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -64,8 +64,8 @@ class DragResizeInputListener implements AutoCloseable { private final TaskResizeInputEventReceiver mInputEventReceiver; private final DragPositioningCallback mCallback; - private int mWidth; - private int mHeight; + private int mTaskWidth; + private int mTaskHeight; private int mResizeHandleThickness; private int mCornerSize; @@ -128,78 +128,84 @@ class DragResizeInputListener implements AutoCloseable { * This is also used to update the touch regions of this handler every event dispatched here is * a potential resize request. * - * @param width The width of the drag resize handler in pixels, including resize handle - * thickness. That is task width + 2 * resize handle thickness. - * @param height The height of the drag resize handler in pixels, including resize handle - * thickness. That is task height + 2 * resize handle thickness. + * @param taskWidth The width of the task. + * @param taskHeight The height of the task. * @param resizeHandleThickness The thickness of the resize handle in pixels. * @param cornerSize The size of the resize handle centered in each corner. * @param touchSlop The distance in pixels user has to drag with touch for it to register as * a resize action. */ - void setGeometry(int width, int height, int resizeHandleThickness, int cornerSize, + void setGeometry(int taskWidth, int taskHeight, int resizeHandleThickness, int cornerSize, int touchSlop) { - if (mWidth == width && mHeight == height + if (mTaskWidth == taskWidth && mTaskHeight == taskHeight && mResizeHandleThickness == resizeHandleThickness && mCornerSize == cornerSize) { return; } - mWidth = width; - mHeight = height; + mTaskWidth = taskWidth; + mTaskHeight = taskHeight; mResizeHandleThickness = resizeHandleThickness; mCornerSize = cornerSize; mDragDetector.setTouchSlop(touchSlop); Region touchRegion = new Region(); - final Rect topInputBounds = new Rect(0, 0, mWidth, mResizeHandleThickness); + final Rect topInputBounds = new Rect( + -mResizeHandleThickness, + -mResizeHandleThickness, + mTaskWidth + mResizeHandleThickness, + 0); touchRegion.union(topInputBounds); - final Rect leftInputBounds = new Rect(0, mResizeHandleThickness, - mResizeHandleThickness, mHeight - mResizeHandleThickness); + final Rect leftInputBounds = new Rect( + -mResizeHandleThickness, + 0, + 0, + mTaskHeight); touchRegion.union(leftInputBounds); final Rect rightInputBounds = new Rect( - mWidth - mResizeHandleThickness, mResizeHandleThickness, - mWidth, mHeight - mResizeHandleThickness); + mTaskWidth, + 0, + mTaskWidth + mResizeHandleThickness, + mTaskHeight); touchRegion.union(rightInputBounds); - final Rect bottomInputBounds = new Rect(0, mHeight - mResizeHandleThickness, - mWidth, mHeight); + final Rect bottomInputBounds = new Rect( + -mResizeHandleThickness, + mTaskHeight, + mTaskWidth + mResizeHandleThickness, + mTaskHeight + mResizeHandleThickness); touchRegion.union(bottomInputBounds); // Set up touch areas in each corner. int cornerRadius = mCornerSize / 2; mLeftTopCornerBounds = new Rect( - mResizeHandleThickness - cornerRadius, - mResizeHandleThickness - cornerRadius, - mResizeHandleThickness + cornerRadius, - mResizeHandleThickness + cornerRadius - ); + -cornerRadius, + -cornerRadius, + cornerRadius, + cornerRadius); touchRegion.union(mLeftTopCornerBounds); mRightTopCornerBounds = new Rect( - mWidth - mResizeHandleThickness - cornerRadius, - mResizeHandleThickness - cornerRadius, - mWidth - mResizeHandleThickness + cornerRadius, - mResizeHandleThickness + cornerRadius - ); + mTaskWidth - cornerRadius, + -cornerRadius, + mTaskWidth + cornerRadius, + cornerRadius); touchRegion.union(mRightTopCornerBounds); mLeftBottomCornerBounds = new Rect( - mResizeHandleThickness - cornerRadius, - mHeight - mResizeHandleThickness - cornerRadius, - mResizeHandleThickness + cornerRadius, - mHeight - mResizeHandleThickness + cornerRadius - ); + -cornerRadius, + mTaskHeight - cornerRadius, + cornerRadius, + mTaskHeight + cornerRadius); touchRegion.union(mLeftBottomCornerBounds); mRightBottomCornerBounds = new Rect( - mWidth - mResizeHandleThickness - cornerRadius, - mHeight - mResizeHandleThickness - cornerRadius, - mWidth - mResizeHandleThickness + cornerRadius, - mHeight - mResizeHandleThickness + cornerRadius - ); + mTaskWidth - cornerRadius, + mTaskHeight - cornerRadius, + mTaskWidth + cornerRadius, + mTaskHeight + cornerRadius); touchRegion.union(mRightBottomCornerBounds); try { @@ -358,16 +364,16 @@ class DragResizeInputListener implements AutoCloseable { @TaskPositioner.CtrlType private int calculateResizeHandlesCtrlType(float x, float y) { int ctrlType = 0; - if (x < mResizeHandleThickness) { + if (x < 0) { ctrlType |= TaskPositioner.CTRL_TYPE_LEFT; } - if (x > mWidth - mResizeHandleThickness) { + if (x > mTaskWidth) { ctrlType |= TaskPositioner.CTRL_TYPE_RIGHT; } - if (y < mResizeHandleThickness) { + if (y < 0) { ctrlType |= TaskPositioner.CTRL_TYPE_TOP; } - if (y > mHeight - mResizeHandleThickness) { + if (y > mTaskHeight) { ctrlType |= TaskPositioner.CTRL_TYPE_BOTTOM; } return ctrlType; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 4ebd09fdecee..bc5fd4dcbdc8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -98,7 +98,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> private final Binder mOwner = new Binder(); private final Rect mCaptionInsetsRect = new Rect(); - private final Rect mTaskSurfaceCrop = new Rect(); private final float[] mTmpColor = new float[3]; WindowDecoration( @@ -218,21 +217,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); final Resources resources = mDecorWindowContext.getResources(); - outResult.mDecorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); - outResult.mDecorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); - outResult.mWidth = taskBounds.width() - + loadDimensionPixelSize(resources, params.mOutsetRightId) - - outResult.mDecorContainerOffsetX; - outResult.mHeight = taskBounds.height() - + loadDimensionPixelSize(resources, params.mOutsetBottomId) - - outResult.mDecorContainerOffsetY; - startT.setPosition( - mDecorationContainerSurface, - outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY) - .setWindowCrop(mDecorationContainerSurface, - outResult.mWidth, outResult.mHeight) + outResult.mWidth = taskBounds.width(); + outResult.mHeight = taskBounds.height(); + startT.setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) .show(mDecorationContainerSurface); + // TODO(b/270202228): This surface can be removed. Instead, use + // |mDecorationContainerSurface| to set the background now that it no longer has outsets + // and its crop is set to the task bounds. // TaskBackgroundSurface if (mTaskBackgroundSurface == null) { final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); @@ -250,8 +242,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f; mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f; mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f; - startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), - taskBounds.height()) + startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height()) .setShadowRadius(mTaskBackgroundSurface, shadowRadius) .setColor(mTaskBackgroundSurface, mTmpColor) .show(mTaskBackgroundSurface); @@ -269,11 +260,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); final int captionWidth = taskBounds.width(); - startT.setPosition( - mCaptionContainerSurface, - -outResult.mDecorContainerOffsetX + params.mCaptionX, - -outResult.mDecorContainerOffsetY + params.mCaptionY) - .setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight) + startT.setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight) .show(mCaptionContainerSurface); if (mCaptionWindowManager == null) { @@ -314,14 +301,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Task surface itself Point taskPosition = mTaskInfo.positionInParent; - mTaskSurfaceCrop.set( - outResult.mDecorContainerOffsetX, - outResult.mDecorContainerOffsetY, - outResult.mWidth + outResult.mDecorContainerOffsetX, - outResult.mHeight + outResult.mDecorContainerOffsetY); startT.show(mTaskSurface); finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y) - .setCrop(mTaskSurface, mTaskSurfaceCrop); + .setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight); } /** @@ -447,37 +429,15 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mCaptionWidthId; int mShadowRadiusId; - int mOutsetTopId; - int mOutsetBottomId; - int mOutsetLeftId; - int mOutsetRightId; - int mCaptionX; int mCaptionY; - void setOutsets(int leftId, int topId, int rightId, int bottomId) { - mOutsetLeftId = leftId; - mOutsetTopId = topId; - mOutsetRightId = rightId; - mOutsetBottomId = bottomId; - } - - void setCaptionPosition(int left, int top) { - mCaptionX = left; - mCaptionY = top; - } - void reset() { mLayoutResId = Resources.ID_NULL; mCaptionHeightId = Resources.ID_NULL; mCaptionWidthId = Resources.ID_NULL; mShadowRadiusId = Resources.ID_NULL; - mOutsetTopId = Resources.ID_NULL; - mOutsetBottomId = Resources.ID_NULL; - mOutsetLeftId = Resources.ID_NULL; - mOutsetRightId = Resources.ID_NULL; - mCaptionX = 0; mCaptionY = 0; } @@ -487,14 +447,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mWidth; int mHeight; T mRootView; - int mDecorContainerOffsetX; - int mDecorContainerOffsetY; void reset() { mWidth = 0; mHeight = 0; - mDecorContainerOffsetX = 0; - mDecorContainerOffsetY = 0; mRootView = null; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index dfa3c1010eed..e8147ff264cc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -159,14 +159,8 @@ public class WindowDecorationTests extends ShellTestCase { .setVisible(false) .build(); taskInfo.isFocused = false; - // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is - // 64px. + // Density is 2. Shadow radius is 10px. Caption height is 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets( - R.dimen.test_window_decor_left_outset, - R.dimen.test_window_decor_top_outset, - R.dimen.test_window_decor_right_outset, - R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -213,14 +207,8 @@ public class WindowDecorationTests extends ShellTestCase { .setVisible(true) .build(); taskInfo.isFocused = true; - // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is - // 64px. + // Density is 2. Shadow radius is 10px. Caption height is 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets( - R.dimen.test_window_decor_left_outset, - R.dimen.test_window_decor_top_outset, - R.dimen.test_window_decor_right_outset, - R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -229,8 +217,7 @@ public class WindowDecorationTests extends ShellTestCase { verify(decorContainerSurfaceBuilder).setParent(taskSurface); verify(decorContainerSurfaceBuilder).setContainerLayer(); verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true); - verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40); - verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220); + verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 300, 100); verify(taskBackgroundSurfaceBuilder).setParent(taskSurface); verify(taskBackgroundSurfaceBuilder).setEffectLayer(); @@ -244,7 +231,6 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); verify(captionContainerSurfaceBuilder).setContainerLayer(); - verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40); verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); @@ -268,12 +254,12 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlFinishT) .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y); verify(mMockSurfaceControlFinishT) - .setCrop(taskSurface, new Rect(-20, -40, 360, 180)); + .setWindowCrop(taskSurface, 300, 100); verify(mMockSurfaceControlStartT) .show(taskSurface); - assertEquals(380, mRelayoutResult.mWidth); - assertEquals(220, mRelayoutResult.mHeight); + assertEquals(300, mRelayoutResult.mWidth); + assertEquals(100, mRelayoutResult.mHeight); } @Test @@ -309,14 +295,8 @@ public class WindowDecorationTests extends ShellTestCase { .setVisible(true) .build(); taskInfo.isFocused = true; - // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is - // 64px. + // Density is 2. Shadow radius is 10px. Caption height is 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets( - R.dimen.test_window_decor_left_outset, - R.dimen.test_window_decor_top_outset, - R.dimen.test_window_decor_right_outset, - R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -419,11 +399,6 @@ public class WindowDecorationTests extends ShellTestCase { .build(); taskInfo.isFocused = true; taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets( - R.dimen.test_window_decor_left_outset, - R.dimen.test_window_decor_top_outset, - R.dimen.test_window_decor_right_outset, - R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); windowDecor.relayout(taskInfo); @@ -438,7 +413,7 @@ public class WindowDecorationTests extends ShellTestCase { verify(additionalWindowSurfaceBuilder).setContainerLayer(); verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface); verify(additionalWindowSurfaceBuilder).build(); - verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 20, 40); + verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 0, 0); final int width = WindowDecoration.loadDimensionPixelSize( mContext.getResources(), mCaptionMenuWidthId); final int height = WindowDecoration.loadDimensionPixelSize( @@ -496,11 +471,6 @@ public class WindowDecorationTests extends ShellTestCase { .build(); taskInfo.isFocused = true; taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mRelayoutParams.setOutsets( - R.dimen.test_window_decor_left_outset, - R.dimen.test_window_decor_top_outset, - R.dimen.test_window_decor_right_outset, - R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -508,7 +478,6 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); verify(captionContainerSurfaceBuilder).setContainerLayer(); - verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40); // Width of the captionContainerSurface should match the width of TASK_BOUNDS verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); @@ -584,9 +553,7 @@ public class WindowDecorationTests extends ShellTestCase { String name = "Test Window"; WindowDecoration.AdditionalWindow additionalWindow = addWindow(R.layout.desktop_mode_window_decor_handle_menu_app_info_pill, name, - mMockSurfaceControlAddWindowT, - x - mRelayoutResult.mDecorContainerOffsetX, - y - mRelayoutResult.mDecorContainerOffsetY, + mMockSurfaceControlAddWindowT, x, y, width, height, shadowRadius, cornerRadius); return additionalWindow; } diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index cc987bcd8f0e..c4d3f5cedfa8 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -53,6 +53,8 @@ SkiaOpenGLPipeline::~SkiaOpenGLPipeline() { } MakeCurrentResult SkiaOpenGLPipeline::makeCurrent() { + bool wasSurfaceless = mEglManager.isCurrent(EGL_NO_SURFACE); + // In case the surface was destroyed (e.g. a previous trimMemory call) we // need to recreate it here. if (mHardwareBuffer) { @@ -65,6 +67,37 @@ MakeCurrentResult SkiaOpenGLPipeline::makeCurrent() { if (!mEglManager.makeCurrent(mEglSurface, &error)) { return MakeCurrentResult::AlreadyCurrent; } + + // Make sure read/draw buffer state of default framebuffer is GL_BACK. Vendor implementations + // disagree on the draw/read buffer state if the default framebuffer transitions from a surface + // to EGL_NO_SURFACE and vice-versa. There was a related discussion within Khronos on this topic. + // See https://cvs.khronos.org/bugzilla/show_bug.cgi?id=13534. + // The discussion was not resolved with a clear consensus + if (error == 0 && wasSurfaceless && mEglSurface != EGL_NO_SURFACE) { + GLint curReadFB = 0; + GLint curDrawFB = 0; + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &curReadFB); + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &curDrawFB); + + GLint buffer = GL_NONE; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glGetIntegerv(GL_DRAW_BUFFER0, &buffer); + if (buffer == GL_NONE) { + const GLenum drawBuffer = GL_BACK; + glDrawBuffers(1, &drawBuffer); + } + + glGetIntegerv(GL_READ_BUFFER, &buffer); + if (buffer == GL_NONE) { + glReadBuffer(GL_BACK); + } + + glBindFramebuffer(GL_READ_FRAMEBUFFER, curReadFB); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, curDrawFB); + + GL_CHECKPOINT(LOW); + } + return error ? MakeCurrentResult::Failed : MakeCurrentResult::Succeeded; } @@ -104,7 +137,8 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( GrBackendRenderTarget backendRT(frame.width(), frame.height(), 0, STENCIL_BUFFER_SIZE, fboInfo); - SkSurfaceProps props(0, kUnknown_SkPixelGeometry); + SkSurfaceProps props(mColorMode == ColorMode::Default ? 0 : SkSurfaceProps::kAlwaysDither_Flag, + kUnknown_SkPixelGeometry); SkASSERT(mRenderThread.getGrContext() != nullptr); sk_sp<SkSurface> surface; diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 1f929685b62c..b020e966e05a 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -28,7 +28,6 @@ #include <SkMultiPictureDocument.h> #include <SkOverdrawCanvas.h> #include <SkOverdrawColorFilter.h> -#include <SkPaintFilterCanvas.h> #include <SkPicture.h> #include <SkPictureRecorder.h> #include <SkRect.h> @@ -450,23 +449,6 @@ void SkiaPipeline::endCapture(SkSurface* surface) { } } -class ForceDitherCanvas : public SkPaintFilterCanvas { -public: - ForceDitherCanvas(SkCanvas* canvas) : SkPaintFilterCanvas(canvas) {} - -protected: - bool onFilter(SkPaint& paint) const override { - paint.setDither(true); - return true; - } - - void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override { - // We unroll the drawable using "this" canvas, so that draw calls contained inside will - // get dithering applied - drawable->draw(this, matrix); - } -}; - void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& clip, const std::vector<sp<RenderNode>>& nodes, bool opaque, const Rect& contentDrawBounds, sk_sp<SkSurface> surface, @@ -521,12 +503,6 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip, canvas->clear(SK_ColorTRANSPARENT); } - std::optional<ForceDitherCanvas> forceDitherCanvas; - if (shouldForceDither()) { - forceDitherCanvas.emplace(canvas); - canvas = &forceDitherCanvas.value(); - } - if (1 == nodes.size()) { if (!nodes[0]->nothingToDraw()) { RenderNodeDrawable root(nodes[0].get(), canvas); diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index 0763b06b53ef..befee8989383 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -98,8 +98,6 @@ protected: bool isCapturingSkp() const { return mCaptureMode != CaptureMode::None; } - virtual bool shouldForceDither() const { return mColorMode != ColorMode::Default; } - private: void renderFrameImpl(const SkRect& clip, const std::vector<sp<RenderNode>>& nodes, bool opaque, diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index f22652f92c15..86096d5bd01c 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -203,11 +203,6 @@ sk_sp<Bitmap> SkiaVulkanPipeline::allocateHardwareBitmap(renderthread::RenderThr return nullptr; } -bool SkiaVulkanPipeline::shouldForceDither() const { - if (mVkSurface && mVkSurface->isBeyond8Bit()) return false; - return SkiaPipeline::shouldForceDither(); -} - void SkiaVulkanPipeline::onContextDestroyed() { if (mVkSurface) { vulkanManager().destroySurface(mVkSurface); diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index cce5468e68f0..284cde537ec0 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -63,8 +63,6 @@ public: protected: void onContextDestroyed() override; - bool shouldForceDither() const override; - private: renderthread::VulkanManager& vulkanManager(); renderthread::VulkanSurface* mVkSurface = nullptr; diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp index 2b7fa0420cef..10f456745147 100644 --- a/libs/hwui/renderthread/VulkanSurface.cpp +++ b/libs/hwui/renderthread/VulkanSurface.cpp @@ -453,9 +453,15 @@ VulkanSurface::NativeBufferInfo* VulkanSurface::dequeueNativeBuffer() { VulkanSurface::NativeBufferInfo* bufferInfo = &mNativeBuffers[idx]; if (bufferInfo->skSurface.get() == nullptr) { + SkSurfaceProps surfaceProps; + if (mWindowInfo.colorMode != ColorMode::Default) { + surfaceProps = SkSurfaceProps(SkSurfaceProps::kAlwaysDither_Flag | surfaceProps.flags(), + surfaceProps.pixelGeometry()); + } bufferInfo->skSurface = SkSurface::MakeFromAHardwareBuffer( mGrContext, ANativeWindowBuffer_getHardwareBuffer(bufferInfo->buffer.get()), - kTopLeft_GrSurfaceOrigin, mWindowInfo.colorspace, nullptr, /*from_window=*/true); + kTopLeft_GrSurfaceOrigin, mWindowInfo.colorspace, &surfaceProps, + /*from_window=*/true); if (bufferInfo->skSurface.get() == nullptr) { ALOGE("SkSurface::MakeFromAHardwareBuffer failed"); mNativeWindow->cancelBuffer(mNativeWindow.get(), buffer, @@ -545,16 +551,6 @@ void VulkanSurface::setColorSpace(sk_sp<SkColorSpace> colorSpace) { } } -bool VulkanSurface::isBeyond8Bit() const { - switch (mWindowInfo.bufferFormat) { - case AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM: - case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT: - return true; - default: - return false; - } -} - } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/VulkanSurface.h b/libs/hwui/renderthread/VulkanSurface.h index d3266af81437..6f5280105e55 100644 --- a/libs/hwui/renderthread/VulkanSurface.h +++ b/libs/hwui/renderthread/VulkanSurface.h @@ -49,8 +49,6 @@ public: void setColorSpace(sk_sp<SkColorSpace> colorSpace); const SkM44& getPixelSnapMatrix() const { return mWindowInfo.pixelSnapMatrix; } - bool isBeyond8Bit() const; - private: /* * All structs/methods in this private section are specifically for use by the VulkanManager diff --git a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java new file mode 100644 index 000000000000..80bc5c07dd66 --- /dev/null +++ b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java @@ -0,0 +1,630 @@ +/** + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.soundtrigger; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.TestApi; +import android.hardware.soundtrigger.ConversionUtil; +import android.hardware.soundtrigger.SoundTrigger; +import android.media.soundtrigger_middleware.IAcknowledgeEvent; +import android.media.soundtrigger_middleware.IInjectGlobalEvent; +import android.media.soundtrigger_middleware.IInjectModelEvent; +import android.media.soundtrigger_middleware.IInjectRecognitionEvent; +import android.media.soundtrigger_middleware.ISoundTriggerInjection; +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.app.ISoundTriggerService; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Used to inject/observe events when using a fake SoundTrigger HAL for test purposes. + * Created by {@link SoundTriggerManager#getInjection(Executor, GlobalCallback)}. + * Only one instance of this class is valid at any given time, old instances will be delivered + * {@link GlobalCallback#onPreempted()}. + * @hide + */ +@TestApi +public final class SoundTriggerInstrumentation { + + private final Object mLock = new Object(); + @GuardedBy("mLock") + private IInjectGlobalEvent mInjectGlobalEvent = null; + + @GuardedBy("mLock") + private Map<IBinder, ModelSession> mModelSessionMap = new HashMap<>(); + @GuardedBy("mLock") + private Map<IBinder, RecognitionSession> mRecognitionSessionMap = new HashMap<>(); + @GuardedBy("mLock") + private IBinder mClientToken = null; + + private final GlobalCallback mClientCallback; + private final Executor mGlobalCallbackExecutor; + + /** + * Callback interface for un-sessioned events observed from the fake STHAL. + * Registered upon construction of {@link SoundTriggerInstrumentation} + * @hide + */ + @TestApi + public interface GlobalCallback { + /** + * Called when the created {@link SoundTriggerInstrumentation} object is invalidated + * by another client creating an {@link SoundTriggerInstrumentation} to instrument the + * fake STHAL. Only one client may inject at a time. + * All sessions are invalidated, no further events will be received, and no + * injected events will be delivered. + */ + default void onPreempted() {} + /** + * Called when the STHAL has been restarted by the framework, due to unexpected + * error conditions. + * Not called when {@link SoundTriggerInstrumentation#triggerRestart()} is injected. + */ + default void onRestarted() {} + /** + * Called when the framework detaches from the fake HAL. + * This is not transmitted to real HALs, but it indicates that the + * framework has flushed its global state. + */ + default void onFrameworkDetached() {} + /** + * Called when a client application attaches to the framework. + * This is not transmitted to real HALs, but it represents the state of + * the framework. + */ + default void onClientAttached() {} + /** + * Called when a client application detaches from the framework. + * This is not transmitted to real HALs, but it represents the state of + * the framework. + */ + default void onClientDetached() {} + /** + * Called when the fake HAL receives a model load from the framework. + * @param modelSession - A session which exposes additional injection + * functionality associated with the newly loaded + * model. See {@link ModelSession}. + */ + void onModelLoaded(@NonNull ModelSession modelSession); + } + + /** + * Callback for HAL events related to a loaded model. Register with + * {@link ModelSession#setModelCallback(Executor, ModelCallback)} + * Note, callbacks will not be delivered for events triggered by the injection. + * @hide + */ + @TestApi + public interface ModelCallback { + /** + * Called when the model associated with the {@link ModelSession} this callback + * was registered for was unloaded by the framework. + */ + default void onModelUnloaded() {} + /** + * Called when the model associated with the {@link ModelSession} this callback + * was registered for receives a set parameter call from the framework. + * @param param - Parameter being set. + * See {@link SoundTrigger.ModelParamTypes} + * @param value - Value the model parameter was set to. + */ + default void onParamSet(@SoundTrigger.ModelParamTypes int param, int value) {} + /** + * Called when the model associated with the {@link ModelSession} this callback + * was registered for receives a recognition start request. + * @param recognitionSession - A session which exposes additional injection + * functionality associated with the newly started + * recognition. See {@link RecognitionSession} + */ + void onRecognitionStarted(@NonNull RecognitionSession recognitionSession); + } + + /** + * Callback for HAL events related to a started recognition. Register with + * {@link RecognitionSession#setRecognitionCallback(Executor, RecognitionCallback)} + * Note, callbacks will not be delivered for events triggered by the injection. + * @hide + */ + @TestApi + public interface RecognitionCallback { + /** + * Called when the recognition associated with the {@link RecognitionSession} this + * callback was registered for was stopped by the framework. + */ + void onRecognitionStopped(); + } + + /** + * Session associated with a loaded model in the fake STHAL. + * Can be used to query details about the loaded model, register a callback for future + * model events, or trigger HAL events associated with a loaded model. + * This session is invalid once the model is unloaded, caused by a + * {@link ModelSession#triggerUnloadModel()}, + * the client unloading recognition, or if a {@link GlobalCallback#onRestarted()} is + * received. + * Further injections on an invalidated session will not be respected, and no future + * callbacks will be delivered. + * @hide + */ + @TestApi + public class ModelSession { + + /** + * Trigger the HAL to preemptively unload the model associated with this session. + * Typically occurs when a higher priority model is loaded which utilizes the same + * resources. + */ + public void triggerUnloadModel() { + synchronized (SoundTriggerInstrumentation.this.mLock) { + try { + mInjectModelEvent.triggerUnloadModel(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mModelSessionMap.remove(mInjectModelEvent.asBinder()); + } + } + + /** + * Get the {@link SoundTriggerManager.Model} associated with this session. + * @return - The model associated with this session. + */ + public @NonNull SoundTriggerManager.Model getSoundModel() { + return mModel; + } + + /** + * Get the list of {@link SoundTrigger.Keyphrase} associated with this session. + * @return - The keyphrases associated with this session. + */ + public @NonNull List<SoundTrigger.Keyphrase> getPhrases() { + if (mPhrases == null) { + return new ArrayList<>(); + } else { + return new ArrayList<>(Arrays.asList(mPhrases)); + } + } + + /** + * Get whether this model is of keyphrase type. + * @return - true if the model is a keyphrase model, false otherwise + */ + public boolean isKeyphrase() { + return (mPhrases != null); + } + + /** + * Registers the model callback associated with this session. Events associated + * with this model session will be reported via this callback. + * See {@link ModelCallback} + * @param executor - Executor which the callback is dispatched on + * @param callback - Model callback for reporting model session events. + */ + public void setModelCallback(@NonNull @CallbackExecutor Executor executor, @NonNull + ModelCallback callback) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + mModelCallback = Objects.requireNonNull(callback); + mModelExecutor = Objects.requireNonNull(executor); + } + } + + /** + * Clear the model callback associated with this session, if any has been + * set by {@link #setModelCallback(Executor, ModelCallback)}. + */ + public void clearModelCallback() { + synchronized (SoundTriggerInstrumentation.this.mLock) { + mModelCallback = null; + mModelExecutor = null; + } + } + + private ModelSession(SoundModel model, Phrase[] phrases, + IInjectModelEvent injection) { + mModel = SoundTriggerManager.Model.create(UUID.fromString(model.uuid), + UUID.fromString(model.vendorUuid), + ConversionUtil.sharedMemoryToByteArray(model.data, model.dataSize)); + if (phrases != null) { + mPhrases = new SoundTrigger.Keyphrase[phrases.length]; + int i = 0; + for (var phrase : phrases) { + mPhrases[i++] = ConversionUtil.aidl2apiPhrase(phrase); + } + } else { + mPhrases = null; + } + mInjectModelEvent = injection; + } + + private void wrap(Consumer<ModelCallback> consumer) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + if (mModelCallback != null && mModelExecutor != null) { + final ModelCallback callback = mModelCallback; + mModelExecutor.execute(() -> consumer.accept(callback)); + } + } + } + + private final SoundTriggerManager.Model mModel; + private final SoundTrigger.Keyphrase[] mPhrases; + private final IInjectModelEvent mInjectModelEvent; + + @GuardedBy("SoundTriggerInstrumentation.this.mLock") + private ModelCallback mModelCallback = null; + @GuardedBy("SoundTriggerInstrumentation.this.mLock") + private Executor mModelExecutor = null; + } + + /** + * Session associated with a recognition start in the fake STHAL. + * Can be used to get information about the started recognition, register a callback + * for future events associated with this recognition, and triggering + * recognition events or aborts. + * This session is invalid once the recognition is stopped, caused by a + * {@link RecognitionSession#triggerAbortRecognition()}, + * {@link RecognitionSession#triggerRecognitionEvent(byte[], List)}, + * the client stopping recognition, or any operation which invalidates the + * {@link ModelSession} which the session was created from. + * Further injections on an invalidated session will not be respected, and no future + * callbacks will be delivered. + * @hide + */ + @TestApi + public class RecognitionSession { + + /** + * Get an integer token representing the audio session associated with this + * recognition in the STHAL. + * @return - The session token. + */ + public int getAudioSession() { + return mAudioSession; + } + + /** + * Get the recognition config used to start this recognition. + * @return - The config passed to the HAL for startRecognition. + */ + public @NonNull SoundTrigger.RecognitionConfig getRecognitionConfig() { + return mRecognitionConfig; + } + + /** + * Trigger a recognition in the fake STHAL. + * @param data - The opaque data buffer included in the recognition event. + * @param phraseExtras - Keyphrase metadata included in the event. The + * event must include metadata for the keyphrase id + * associated with this model to be received by the + * client application. + */ + public void triggerRecognitionEvent(@NonNull byte[] data, @Nullable + List<SoundTrigger.KeyphraseRecognitionExtra> phraseExtras) { + PhraseRecognitionExtra[] converted = null; + if (phraseExtras != null) { + converted = new PhraseRecognitionExtra[phraseExtras.size()]; + int i = 0; + for (var phraseExtra : phraseExtras) { + converted[i++] = ConversionUtil.api2aidlPhraseRecognitionExtra(phraseExtra); + } + } + synchronized (SoundTriggerInstrumentation.this.mLock) { + mRecognitionSessionMap.remove(mInjectRecognitionEvent.asBinder()); + try { + mInjectRecognitionEvent.triggerRecognitionEvent(data, converted); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Trigger an abort recognition event in the fake HAL. This represents a + * preemptive ending of the recognition session by the HAL, despite no + * recognition detection. Typically occurs during contention for microphone + * usage, or if model limits are hit. + * See {@link SoundTriggerInstrumentation#setResourceContention(boolean)} to block + * subsequent downward calls for contention reasons. + */ + public void triggerAbortRecognition() { + synchronized (SoundTriggerInstrumentation.this.mLock) { + mRecognitionSessionMap.remove(mInjectRecognitionEvent.asBinder()); + try { + mInjectRecognitionEvent.triggerAbortRecognition(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Registers the recognition callback associated with this session. Events associated + * with this recognition session will be reported via this callback. + * See {@link RecognitionCallback} + * @param executor - Executor which the callback is dispatched on + * @param callback - Recognition callback for reporting recognition session events. + */ + public void setRecognitionCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull RecognitionCallback callback) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + mRecognitionCallback = callback; + mRecognitionExecutor = executor; + } + } + + /** + * Clear the recognition callback associated with this session, if any has been + * set by {@link #setRecognitionCallback(Executor, RecognitionCallback)}. + */ + public void clearRecognitionCallback() { + synchronized (SoundTriggerInstrumentation.this.mLock) { + mRecognitionCallback = null; + mRecognitionExecutor = null; + } + } + + private RecognitionSession(int audioSession, + RecognitionConfig recognitionConfig, + IInjectRecognitionEvent injectRecognitionEvent) { + mAudioSession = audioSession; + mRecognitionConfig = ConversionUtil.aidl2apiRecognitionConfig(recognitionConfig); + mInjectRecognitionEvent = injectRecognitionEvent; + } + + private void wrap(Consumer<RecognitionCallback> consumer) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + if (mRecognitionCallback != null && mRecognitionExecutor != null) { + final RecognitionCallback callback = mRecognitionCallback; + mRecognitionExecutor.execute(() -> consumer.accept(callback)); + } + } + } + + private final int mAudioSession; + private final SoundTrigger.RecognitionConfig mRecognitionConfig; + private final IInjectRecognitionEvent mInjectRecognitionEvent; + + @GuardedBy("SoundTriggerInstrumentation.this.mLock") + private Executor mRecognitionExecutor = null; + @GuardedBy("SoundTriggerInstrumentation.this.mLock") + private RecognitionCallback mRecognitionCallback = null; + } + + // Implementation of injection interface passed to the HAL. + // This class will re-associate events received on this callback interface + // with sessions, to avoid staleness issues. + private class Injection extends ISoundTriggerInjection.Stub { + @Override + public void registerGlobalEventInjection(IInjectGlobalEvent globalInjection) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + mInjectGlobalEvent = globalInjection; + } + } + + @Override + public void onSoundModelLoaded(SoundModel model, @Nullable Phrase[] phrases, + IInjectModelEvent modelInjection, IInjectGlobalEvent globalSession) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return; + ModelSession modelSession = new ModelSession(model, phrases, modelInjection); + mModelSessionMap.put(modelInjection.asBinder(), modelSession); + mGlobalCallbackExecutor.execute(() -> mClientCallback.onModelLoaded(modelSession)); + } + } + + @Override + public void onSoundModelUnloaded(IInjectModelEvent modelSession) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + ModelSession clientModelSession = mModelSessionMap.remove(modelSession.asBinder()); + if (clientModelSession == null) return; + clientModelSession.wrap((ModelCallback cb) -> cb.onModelUnloaded()); + } + } + + @Override + public void onRecognitionStarted(int audioSessionHandle, RecognitionConfig config, + IInjectRecognitionEvent recognitionInjection, IInjectModelEvent modelSession) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + ModelSession clientModelSession = mModelSessionMap.get(modelSession.asBinder()); + if (clientModelSession == null) return; + RecognitionSession recogSession = new RecognitionSession( + audioSessionHandle, config, recognitionInjection); + mRecognitionSessionMap.put(recognitionInjection.asBinder(), recogSession); + clientModelSession.wrap((ModelCallback cb) -> + cb.onRecognitionStarted(recogSession)); + } + } + + @Override + public void onRecognitionStopped(IInjectRecognitionEvent recognitionSession) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + RecognitionSession clientRecognitionSession = + mRecognitionSessionMap.remove(recognitionSession.asBinder()); + if (clientRecognitionSession == null) return; + clientRecognitionSession.wrap((RecognitionCallback cb) + -> cb.onRecognitionStopped()); + } + } + + @Override + public void onParamSet(int modelParam, int value, IInjectModelEvent modelSession) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + ModelSession clientModelSession = mModelSessionMap.get(modelSession.asBinder()); + if (clientModelSession == null) return; + clientModelSession.wrap((ModelCallback cb) -> cb.onParamSet(modelParam, value)); + } + } + + + @Override + public void onRestarted(IInjectGlobalEvent globalSession) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return; + mRecognitionSessionMap.clear(); + mModelSessionMap.clear(); + mGlobalCallbackExecutor.execute(() -> mClientCallback.onRestarted()); + } + } + + @Override + public void onFrameworkDetached(IInjectGlobalEvent globalSession) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return; + mGlobalCallbackExecutor.execute(() -> mClientCallback.onFrameworkDetached()); + } + } + + @Override + public void onClientAttached(IBinder token, IInjectGlobalEvent globalSession) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return; + mClientToken = token; + mGlobalCallbackExecutor.execute(() -> mClientCallback.onClientAttached()); + } + } + + @Override + public void onClientDetached(IBinder token) { + synchronized (SoundTriggerInstrumentation.this.mLock) { + if (token != mClientToken) return; + mClientToken = null; + mGlobalCallbackExecutor.execute(() -> mClientCallback.onClientDetached()); + } + } + + @Override + public void onPreempted() { + // This is always valid, independent of session + mGlobalCallbackExecutor.execute(() -> mClientCallback.onPreempted()); + // Callbacks will no longer be delivered, and injection will be silently dropped. + } + } + + /** + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) + public SoundTriggerInstrumentation(ISoundTriggerService service, + @CallbackExecutor @NonNull Executor executor, + @NonNull GlobalCallback callback) { + mClientCallback = Objects.requireNonNull(callback); + mGlobalCallbackExecutor = Objects.requireNonNull(executor); + try { + service.attachInjection(new Injection()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Simulate a HAL restart, typically caused by the framework on an unexpected error, + * or a restart of the core audio HAL. + * Application sessions will be detached, and all state will be cleared. The framework + * will re-attach to the HAL following restart. + * @hide + */ + @TestApi + public void triggerRestart() { + synchronized (mLock) { + if (mInjectGlobalEvent == null) { + throw new IllegalStateException( + "Attempted to trigger HAL restart before registration"); + } + try { + mInjectGlobalEvent.triggerRestart(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Trigger a resource available callback from the fake SoundTrigger HAL to the framework. + * This callback notifies the framework that methods which previously failed due to + * resource contention may now succeed. + * @hide + */ + @TestApi + public void triggerOnResourcesAvailable() { + synchronized (mLock) { + if (mInjectGlobalEvent == null) { + throw new IllegalStateException( + "Attempted to trigger HAL resources available before registration"); + } + try { + mInjectGlobalEvent.triggerOnResourcesAvailable(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Simulate resource contention, similar to when HAL which does not + * support concurrent capture opens a capture stream, or when a HAL + * has reached its maximum number of models. + * Subsequent model loads and recognition starts will gracefully error. + * Since this call does not trigger a callback through the framework, the + * call will block until the fake HAL has acknowledged the state change. + * @param isResourceContended - true to enable contention, false to return + * to normal functioning. + * @hide + */ + @TestApi + public void setResourceContention(boolean isResourceContended) { + synchronized (mLock) { + if (mInjectGlobalEvent == null) { + throw new IllegalStateException("Injection interface not set up"); + } + IInjectGlobalEvent current = mInjectGlobalEvent; + final CountDownLatch signal = new CountDownLatch(1); + try { + current.setResourceContention(isResourceContended, new IAcknowledgeEvent.Stub() { + @Override + public void eventReceived() { + signal.countDown(); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + // Block until we get a callback from the service that our request was serviced. + try { + // Rely on test timeout if we don't get a response. + signal.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } +} + diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java index ae8121a59abf..c41bd1bc3094 100644 --- a/media/java/android/media/soundtrigger/SoundTriggerManager.java +++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java @@ -18,11 +18,13 @@ package android.media.soundtrigger; import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.app.ActivityThread; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; @@ -45,6 +47,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; +import android.os.ServiceManager; import android.provider.Settings; import android.util.Slog; @@ -53,9 +56,9 @@ import com.android.internal.app.ISoundTriggerSession; import com.android.internal.util.Preconditions; import java.util.HashMap; -import java.util.List; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.Executor; /** * This class provides management of non-voice (general sound trigger) based sound recognition @@ -609,4 +612,24 @@ public final class SoundTriggerManager { throw e.rethrowFromSystemServer(); } } + + /** + * Create a {@link SoundTriggerInstrumentation} for test purposes, which instruments a fake + * STHAL. Clients must attach to the appropriate underlying ST module. + * @param executor - Executor to dispatch global callbacks on + * @param callback - Callback for unsessioned events received by the fake STHAL + * @return - A {@link SoundTriggerInstrumentation} for observation/injection. + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) + @NonNull + public static SoundTriggerInstrumentation attachInstrumentation( + @CallbackExecutor @NonNull Executor executor, + @NonNull SoundTriggerInstrumentation.GlobalCallback callback) { + ISoundTriggerService service = ISoundTriggerService.Stub.asInterface( + ServiceManager.getService(Context.SOUND_TRIGGER_SERVICE)); + return new SoundTriggerInstrumentation(service, executor, callback); + } + } diff --git a/media/java/android/media/voice/KeyphraseModelManager.java b/media/java/android/media/voice/KeyphraseModelManager.java index 8ec8967a353e..5a690a57dbb2 100644 --- a/media/java/android/media/voice/KeyphraseModelManager.java +++ b/media/java/android/media/voice/KeyphraseModelManager.java @@ -21,7 +21,9 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.hardware.soundtrigger.SoundTrigger; +import android.os.Binder; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.util.Slog; @@ -154,4 +156,23 @@ public final class KeyphraseModelManager { throw e.rethrowFromSystemServer(); } } + + /** + * Override the persistent enrolled model database with an in-memory + * fake for testing purposes. + * + * @param enabled - {@code true} if the model enrollment database should be overridden with an + * in-memory fake. {@code false} if the real, persistent model enrollment database should be + * used. + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES) + @TestApi + public void setModelDatabaseForTestEnabled(boolean enabled) { + try { + mVoiceInteractionManagerService.setModelDatabaseForTestEnabled(enabled, new Binder()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index e9b2e1041c22..3e652517270d 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -117,6 +117,8 @@ <string name="get_dialog_sign_in_type_username_separator" translatable="false">" • "</string> <!-- This text is followed by a list of one or more options. [CHAR LIMIT=80] --> <string name="get_dialog_title_sign_in_options">Sign-in options</string> + <!-- Button label for viewing the full information about an account. [CHAR LIMIT=80] --> + <string name="button_label_view_more">View more</string> <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=80] --> <string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string> <!-- Column heading for displaying locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-ins. [CHAR LIMIT=80] --> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index ca891294576b..64addf237f9b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -252,13 +252,6 @@ class GetFlowUtils { )) } is PublicKeyCredentialEntry -> { - val passkeyUsername = credentialEntry.username.toString() - val passkeyDisplayName = credentialEntry.displayName?.toString() ?: "" - val (username, displayName) = userAndDisplayNameForPasskey( - passkeyUsername = passkeyUsername, - passkeyDisplayName = passkeyDisplayName, - ) - result.add(CredentialEntryInfo( providerId = providerId, providerDisplayName = providerLabel, @@ -268,8 +261,8 @@ class GetFlowUtils { fillInIntent = it.frameworkExtrasIntent, credentialType = CredentialType.PASSKEY, credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(), - userName = username, - displayName = displayName, + userName = credentialEntry.username.toString(), + displayName = credentialEntry.displayName?.toString(), icon = credentialEntry.icon.loadDrawable(context), shouldTintIcon = credentialEntry.isDefaultIcon, lastUsedTimeMillis = credentialEntry.lastUsedTime, @@ -707,7 +700,7 @@ class CreateFlowUtils { * 2) username on top if display-name is not available. * 3) don't show username on second line if username == display-name */ -private fun userAndDisplayNameForPasskey( +fun userAndDisplayNameForPasskey( passkeyUsername: String, passkeyDisplayName: String, ): Pair<String, String> { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index 0623ff629812..2dba2ab6777c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection @@ -79,6 +80,7 @@ fun Entry( /** If true, draws a trailing lock icon. */ isLockedAuthEntry: Boolean = false, enforceOneLine: Boolean = false, + onTextLayout: (TextLayoutResult) -> Unit = {}, ) { val iconPadding = Modifier.wrapContentSize().padding( // Horizontal padding should be 16dp, but the suggestion chip itself @@ -103,7 +105,11 @@ fun Entry( ) { // Apply weight so that the trailing icon can always show. Column(modifier = Modifier.wrapContentHeight().fillMaxWidth().weight(1f)) { - SmallTitleText(text = entryHeadlineText, enforceOneLine = enforceOneLine) + SmallTitleText( + text = entryHeadlineText, + enforceOneLine = enforceOneLine, + onTextLayout = onTextLayout, + ) if (passwordValue != null) { Row( modifier = Modifier.fillMaxWidth().padding(top = 4.dp), @@ -141,10 +147,18 @@ fun Entry( ) } } else if (entrySecondLineText != null) { - BodySmallText(text = entrySecondLineText, enforceOneLine = enforceOneLine) + BodySmallText( + text = entrySecondLineText, + enforceOneLine = enforceOneLine, + onTextLayout = onTextLayout, + ) } if (entryThirdLineText != null) { - BodySmallText(text = entryThirdLineText, enforceOneLine = enforceOneLine) + BodySmallText( + text = entryThirdLineText, + enforceOneLine = enforceOneLine, + onTextLayout = onTextLayout, + ) } } if (isLockedAuthEntry) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt index 61c03b4041f5..6b46636964e4 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme @@ -59,14 +60,20 @@ fun BodyMediumText(text: String, modifier: Modifier = Modifier) { * Body-small typography; on-surface-variant color. */ @Composable -fun BodySmallText(text: String, modifier: Modifier = Modifier, enforceOneLine: Boolean = false) { +fun BodySmallText( + text: String, + modifier: Modifier = Modifier, + enforceOneLine: Boolean = false, + onTextLayout: (TextLayoutResult) -> Unit = {}, +) { Text( modifier = modifier.wrapContentSize(), text = text, color = LocalAndroidColorScheme.current.colorOnSurfaceVariant, style = MaterialTheme.typography.bodySmall, overflow = TextOverflow.Ellipsis, - maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE + maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE, + onTextLayout = onTextLayout, ) } @@ -87,14 +94,20 @@ fun LargeTitleText(text: String, modifier: Modifier = Modifier) { * Title-small typography; on-surface color. */ @Composable -fun SmallTitleText(text: String, modifier: Modifier = Modifier, enforceOneLine: Boolean = false) { +fun SmallTitleText( + text: String, + modifier: Modifier = Modifier, + enforceOneLine: Boolean = false, + onTextLayout: (TextLayoutResult) -> Unit = {}, +) { Text( modifier = modifier.wrapContentSize(), text = text, color = LocalAndroidColorScheme.current.colorOnSurface, style = MaterialTheme.typography.titleSmall, overflow = TextOverflow.Ellipsis, - maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE + maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE, + onTextLayout = onTextLayout, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 6d7ecd70eb0b..b8b7ac33bd9f 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -34,11 +34,15 @@ import androidx.compose.material3.Divider import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.CredentialSelectorViewModel @@ -62,6 +66,7 @@ import com.android.credentialmanager.common.ui.HeadlineIcon import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant import com.android.credentialmanager.common.ui.Snackbar import com.android.credentialmanager.logging.GetCredentialEvent +import com.android.credentialmanager.userAndDisplayNameForPasskey import com.android.internal.logging.UiEventLogger.UiEventEnum @Composable @@ -158,6 +163,7 @@ fun PrimarySelectionCard( onMoreOptionSelected: () -> Unit, onLog: @Composable (UiEventEnum) -> Unit, ) { + val showMoreForTruncatedEntry = remember { mutableStateOf(false) } val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList val authenticationEntryList = providerDisplayInfo.authenticationEntryList @@ -209,6 +215,8 @@ fun PrimarySelectionCard( Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { val usernameForCredentialSize = sortedUserNameToCredentialEntryList.size val authenticationEntrySize = authenticationEntryList.size + // If true, render a view more button for the single truncated entry on the + // front page. // Show max 4 entries in this primary page if (usernameForCredentialSize + authenticationEntrySize <= 4) { sortedUserNameToCredentialEntryList.forEach { @@ -216,6 +224,9 @@ fun PrimarySelectionCard( credentialEntryInfo = it.sortedCredentialEntryList.first(), onEntrySelected = onEntrySelected, enforceOneLine = true, + onTextLayout = { + showMoreForTruncatedEntry.value = it.hasVisualOverflow + } ) } authenticationEntryList.forEach { @@ -269,6 +280,13 @@ fun PrimarySelectionCard( onMoreOptionSelected ) } + } else if (showMoreForTruncatedEntry.value) { + { + ActionButton( + stringResource(R.string.button_label_view_more), + onMoreOptionSelected + ) + } } else null, rightButton = if (activeEntry != null) { // Only one sign-in options exist { @@ -438,7 +456,12 @@ fun CredentialEntryRow( credentialEntryInfo: CredentialEntryInfo, onEntrySelected: (BaseEntry) -> Unit, enforceOneLine: Boolean = false, + onTextLayout: (TextLayoutResult) -> Unit = {}, ) { + val (username, displayName) = if (credentialEntryInfo.credentialType == CredentialType.PASSKEY) + userAndDisplayNameForPasskey( + credentialEntryInfo.userName, credentialEntryInfo.displayName ?: "") + else Pair(credentialEntryInfo.userName, credentialEntryInfo.displayName) Entry( onClick = { onEntrySelected(credentialEntryInfo) }, iconImageBitmap = credentialEntryInfo.icon?.toBitmap()?.asImageBitmap(), @@ -447,13 +470,13 @@ fun CredentialEntryRow( iconPainter = if (credentialEntryInfo.icon == null) painterResource(R.drawable.ic_other_sign_in_24) else null, - entryHeadlineText = credentialEntryInfo.userName, + entryHeadlineText = username, entrySecondLineText = if ( credentialEntryInfo.credentialType == CredentialType.PASSWORD) { "••••••••••••" } else { val itemsToDisplay = listOf( - credentialEntryInfo.displayName, + displayName, credentialEntryInfo.credentialTypeDisplayName, credentialEntryInfo.providerDisplayName ).filterNot(TextUtils::isEmpty) @@ -463,6 +486,7 @@ fun CredentialEntryRow( ) }, enforceOneLine = enforceOneLine, + onTextLayout = onTextLayout, ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt index 47ac2df67c76..e07a6298a627 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt @@ -69,6 +69,7 @@ import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp @@ -460,6 +461,9 @@ private fun TopAppBarLayout( ProvideTextStyle(value = titleTextStyle) { CompositionLocalProvider( LocalContentColor provides titleContentColor, + // Disable the title font scaling by only passing the density but not the + // font scale. + LocalDensity provides Density(density = LocalDensity.current.density), content = title ) } diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java index a3db6d7e17ff..e28ada4771c9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java @@ -120,9 +120,9 @@ public class BatterySaverUtils { * @return true if the request succeeded. */ public static synchronized boolean setPowerSaveMode(Context context, - boolean enable, boolean needFirstTimeWarning) { + boolean enable, boolean needFirstTimeWarning, @SaverManualEnabledReason int reason) { if (DEBUG) { - Log.d(TAG, "Battery saver turning " + (enable ? "ON" : "OFF")); + Log.d(TAG, "Battery saver turning " + (enable ? "ON" : "OFF") + ", reason: " + reason); } final ContentResolver cr = context.getContentResolver(); @@ -152,6 +152,7 @@ public class BatterySaverUtils { sendSystemUiBroadcast(context, ACTION_SHOW_AUTO_SAVER_SUGGESTION, confirmationExtras); } + recordBatterySaverEnabledReason(context, reason); } return true; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java index ad022a63eaf6..cb386fbff4ed 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java @@ -16,6 +16,7 @@ package com.android.settingslib.fuelgauge; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_UNKNOWN; import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_NO_SCHEDULE; import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE; @@ -72,7 +73,8 @@ public class BatterySaverUtilsTest { Secure.putString(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, "null"); Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); - assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, true, true)).isFalse(); + assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, true, true, + SAVER_ENABLED_UNKNOWN)).isFalse(); verify(mMockContext, times(1)).sendBroadcast(any(Intent.class)); verify(mMockPowerManager, times(0)).setPowerSaveModeEnabled(anyBoolean()); @@ -92,7 +94,8 @@ public class BatterySaverUtilsTest { Secure.putInt(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, 1); Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); - assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, true, true)).isTrue(); + assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, true, true, + SAVER_ENABLED_UNKNOWN)).isTrue(); verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); verify(mMockPowerManager, times(1)).setPowerSaveModeEnabled(eq(true)); @@ -111,7 +114,8 @@ public class BatterySaverUtilsTest { Secure.putInt(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, 1); Secure.putInt(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, 1); - assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, true, true)).isTrue(); + assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, true, true, + SAVER_ENABLED_UNKNOWN)).isTrue(); verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); verify(mMockPowerManager, times(1)).setPowerSaveModeEnabled(eq(true)); @@ -129,7 +133,8 @@ public class BatterySaverUtilsTest { Secure.putString(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, "null"); Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); - assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, true, false)).isTrue(); + assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, true, false, + SAVER_ENABLED_UNKNOWN)).isTrue(); verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); verify(mMockPowerManager, times(1)).setPowerSaveModeEnabled(eq(true)); @@ -147,7 +152,8 @@ public class BatterySaverUtilsTest { Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); // When disabling, needFirstTimeWarning doesn't matter. - assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, false, false)).isTrue(); + assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, false, false, + SAVER_ENABLED_UNKNOWN)).isTrue(); verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); verify(mMockPowerManager, times(1)).setPowerSaveModeEnabled(eq(false)); @@ -166,7 +172,8 @@ public class BatterySaverUtilsTest { Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null"); // When disabling, needFirstTimeWarning doesn't matter. - assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, false, true)).isTrue(); + assertThat(BatterySaverUtils.setPowerSaveMode(mMockContext, false, true, + SAVER_ENABLED_UNKNOWN)).isTrue(); verify(mMockContext, times(0)).sendBroadcast(any(Intent.class)); verify(mMockPowerManager, times(1)).setPowerSaveModeEnabled(eq(false)); diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 36a0b5dd72b8..dabb5780621c 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -347,15 +347,6 @@ <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> - <!-- Intent Chooser --> - <permission - android:name="android.permission.ADD_CHOOSER_PINS" - android:protectionLevel="signature" /> - <uses-permission android:name="android.permission.ADD_CHOOSER_PINS" /> - <permission - android:name="android.permission.RECEIVE_CHOOSER_PIN_MIGRATION" - android:protectionLevel="signature" /> - <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt index 52dfc55c105d..f71c137363c5 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt @@ -4,7 +4,7 @@ import android.os.Bundle import androidx.annotation.VisibleForTesting class WeatherData -private constructor( +constructor( val description: String, val state: WeatherStateIcon, val useCelsius: Boolean, diff --git a/packages/SystemUI/res-keyguard/drawable/fp_to_locked.xml b/packages/SystemUI/res-keyguard/drawable/fp_to_locked.xml new file mode 100644 index 000000000000..61a1cb5f1d31 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/fp_to_locked.xml @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<animated-vector + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="65dp" android:width="46dp" android:viewportHeight="65" android:viewportWidth="46"> + <group android:name="_R_G"> + <group android:name="_R_G_L_1_G" android:translateX="3.75" android:translateY="8.25"> + <path android:name="_R_G_L_1_G_D_0_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 "/> + <path android:name="_R_G_L_1_G_D_1_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 "/> + <path android:name="_R_G_L_1_G_D_2_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 "/> + <path android:name="_R_G_L_1_G_D_3_P_0" android:strokeColor="#FF000000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 "/> + </group> + <group android:name="_R_G_L_0_G" android:translateX="20.357" android:translateY="35.75" android:pivotX="2.75" android:pivotY="2.75" android:scaleX="1.41866" android:scaleY="1.41866"> + <path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#FF000000" android:fillAlpha="0" android:fillType="nonZero" android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c "/> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="107" android:startOffset="0" android:valueFrom="M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 " android:valueTo="M30.81 32.26 C30.56,32.49 30.27,38.76 29.96,38.9 C29.77,39.46 29.13,39.94 28.57,40.26 C28.15,40.51 26.93,40.65 26.4,40.65 C26.18,40.65 11.91,40.62 11.71,40.58 C10.68,40.53 9.06,39.79 8.89,38.88 C8.6,38.74 8.34,32.48 8.1,32.27 " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.541,0 0.833,0.767 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="143" android:startOffset="107" android:valueFrom="M30.81 32.26 C30.56,32.49 30.27,38.76 29.96,38.9 C29.77,39.46 29.13,39.94 28.57,40.26 C28.15,40.51 26.93,40.65 26.4,40.65 C26.18,40.65 11.91,40.62 11.71,40.58 C10.68,40.53 9.06,39.79 8.89,38.88 C8.6,38.74 8.34,32.48 8.1,32.27 " android:valueTo="M30.64 30.14 C30.64,30.14 30.64,38.14 30.64,38.14 C30.64,38.77 30.36,39.32 29.91,39.69 C29.57,39.97 29.12,40.14 28.64,40.14 C28.64,40.14 10.14,40.14 10.14,40.14 C9.04,40.14 8.14,39.25 8.14,38.14 C8.14,38.14 8.14,30.14 8.14,30.14 " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.233 0.331,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G_D_1_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="strokeAlpha" android:duration="140" android:startOffset="0" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="strokeAlpha" android:duration="50" android:startOffset="140" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G_D_1_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="107" android:startOffset="0" android:valueFrom="M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 " android:valueTo="M18.93 32.18 C17.11,32.68 16.62,30.26 16.62,30.26 C16.62,28.78 17.81,27.59 19.39,27.59 C20.96,27.59 22.15,28.78 22.15,30.26 C22.15,30.26 22.15,30.34 22.15,30.34 C22.15,30.89 21.11,32.54 19.57,32.19 C19.19,32.1 20.48,31.09 20.34,30.71 C20.34,30.71 20.02,29.88 20.02,29.88 C19.88,29.5 19.53,29.25 19.15,29.25 C18.63,29.25 18,29.67 18,30.22 C18,30.57 18.06,31.08 18.32,31.51 C18.49,31.8 19.02,32.25 19.79,32.04 C20.41,31.7 20.38,31.36 20.38,31.36 " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.541,0 0.833,0.767 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="107" android:startOffset="107" android:valueFrom="M18.93 32.18 C17.11,32.68 16.62,30.26 16.62,30.26 C16.62,28.78 17.81,27.59 19.39,27.59 C20.96,27.59 22.15,28.78 22.15,30.26 C22.15,30.26 22.15,30.34 22.15,30.34 C22.15,30.89 21.11,32.54 19.57,32.19 C19.19,32.1 20.48,31.09 20.34,30.71 C20.34,30.71 20.02,29.88 20.02,29.88 C19.88,29.5 19.53,29.25 19.15,29.25 C18.63,29.25 18,29.67 18,30.22 C18,30.57 18.06,31.08 18.32,31.51 C18.49,31.8 19.02,32.25 19.79,32.04 C20.41,31.7 20.38,31.36 20.38,31.36 " android:valueTo="M19.42 31.53 C18.15,31.52 18.11,30.33 18.11,30.33 C18.11,29.59 18.66,28.98 19.4,28.98 C20.13,28.98 20.69,29.59 20.69,30.33 C20.69,30.33 20.69,30.37 20.69,30.37 C20.69,30.64 20.49,30.87 20.25,30.87 C20.07,30.87 19.91,30.74 19.84,30.55 C19.84,30.55 19.69,30.14 19.69,30.14 C19.63,29.94 19.46,29.82 19.28,29.82 C19.04,29.82 18.61,30.02 18.61,30.29 C18.61,30.43 18.6,30.75 18.76,31.03 C18.87,31.21 19.21,31.77 19.96,31.41 C20.69,31.01 20.69,30.34 20.69,30.34 " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.233 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G_D_2_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="250" android:startOffset="0" android:valueFrom="M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 " android:valueTo="M8.14 30.22 C8.14,30.22 8.14,22.22 8.14,22.22 C8.14,21.71 8.33,21.25 8.64,20.9 C9,20.48 9.54,20.22 10.14,20.22 C10.14,20.22 28.64,20.22 28.64,20.22 C29.75,20.22 30.64,21.11 30.64,22.22 C30.64,22.22 30.64,30.14 30.64,30.14 " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.541,0 0.189,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G_D_3_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="95" android:startOffset="0" android:valueFrom="M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 " android:valueTo="M11.47 14.84 C11.47,14.84 12.21,11.43 13.54,9.84 C14.84,8.28 16.68,7.22 19.35,7.22 C22.01,7.22 23.98,8.4 25.19,10.18 C26.39,11.96 27.25,14.84 27.25,14.84 " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.541,0 0.833,0.767 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="24" android:startOffset="95" android:valueFrom="M11.47 14.84 C11.47,14.84 12.21,11.43 13.54,9.84 C14.84,8.28 16.68,7.22 19.35,7.22 C22.01,7.22 23.98,8.4 25.19,10.18 C26.39,11.96 27.25,14.84 27.25,14.84 " android:valueTo="M12.11 16.85 C12.11,16.85 12.82,12.71 13.37,11.5 C14.17,9.24 16.38,7.53 19.35,7.53 C22.32,7.53 24.61,9.32 25.35,11.72 C25.61,12.64 26.62,16.85 26.62,16.85 " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.233 0.833,0.767 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="pathData" android:duration="81" android:startOffset="119" android:valueFrom="M12.11 16.85 C12.11,16.85 12.82,12.71 13.37,11.5 C14.17,9.24 16.38,7.53 19.35,7.53 C22.32,7.53 24.61,9.32 25.35,11.72 C25.61,12.64 26.62,16.85 26.62,16.85 " android:valueTo="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.233 0.261,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="fillAlpha" android:duration="120" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="fillAlpha" android:duration="20" android:startOffset="120" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="scaleX" android:duration="120" android:startOffset="0" android:valueFrom="1.4186600000000003" android:valueTo="1.4186600000000003" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.43,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="120" android:startOffset="0" android:valueFrom="1.4186600000000003" android:valueTo="1.4186600000000003" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.43,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleX" android:duration="130" android:startOffset="120" android:valueFrom="1.4186600000000003" android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.43,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator android:propertyName="scaleY" android:duration="130" android:startOffset="120" android:valueFrom="1.4186600000000003" android:valueTo="1" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.43,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="translateX" android:duration="517" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml b/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml index 951d6fed0a17..f3325ecefada 100644 --- a/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml +++ b/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml @@ -104,4 +104,9 @@ android:fromId="@id/unlocked" android:toId="@id/locked" android:drawable="@drawable/unlocked_to_locked" /> + + <transition + android:fromId="@id/locked_fp" + android:toId="@id/locked" + android:drawable="@drawable/fp_to_locked" /> </animated-selector> diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml index 2143fc4db852..edd3047203b3 100644 --- a/packages/SystemUI/res-keyguard/values/strings.xml +++ b/packages/SystemUI/res-keyguard/values/strings.xml @@ -21,19 +21,19 @@ <!-- Instructions telling the user to enter their PIN password to unlock the keyguard [CHAR LIMIT=30] --> <string name="keyguard_enter_your_pin">Enter your PIN</string> - <!-- Instructions telling the user to enter their PIN password to unlock the keyguard [CHAR LIMIT=26] --> + <!-- Instructions telling the user to enter their PIN password to unlock the keyguard [CHAR LIMIT=48] --> <string name="keyguard_enter_pin">Enter PIN</string> <!-- Instructions telling the user to enter their pattern to unlock the keyguard [CHAR LIMIT=30] --> <string name="keyguard_enter_your_pattern">Enter your pattern</string> - <!-- Instructions telling the user to enter their pattern to unlock the keyguard [CHAR LIMIT=26] --> + <!-- Instructions telling the user to enter their pattern to unlock the keyguard [CHAR LIMIT=48] --> <string name="keyguard_enter_pattern">Draw pattern</string> <!-- Instructions telling the user to enter their text password to unlock the keyguard [CHAR LIMIT=30] --> <string name="keyguard_enter_your_password">Enter your password</string> - <!-- Instructions telling the user to enter their text password to unlock the keyguard [CHAR LIMIT=26] --> + <!-- Instructions telling the user to enter their text password to unlock the keyguard [CHAR LIMIT=48] --> <string name="keyguard_enter_password">Enter password</string> <!-- Shown in the lock screen when there is SIM card IO error. --> @@ -128,103 +128,103 @@ <!-- Message shown when user enters wrong pattern --> <string name="kg_wrong_pattern">Wrong pattern</string> - <!-- Message shown when user enters wrong pattern [CHAR LIMIT=26] --> + <!-- Message shown when user enters wrong pattern [CHAR LIMIT=48] --> <string name="kg_wrong_pattern_try_again">Wrong pattern. Try again.</string> <!-- Message shown when user enters wrong password --> <string name="kg_wrong_password">Wrong password</string> - <!-- Message shown when user enters wrong pattern [CHAR LIMIT=26] --> + <!-- Message shown when user enters wrong pattern [CHAR LIMIT=48] --> <string name="kg_wrong_password_try_again">Wrong password. Try again.</string> <!-- Message shown when user enters wrong PIN --> <string name="kg_wrong_pin">Wrong PIN</string> - <!-- Message shown when user enters wrong PIN [CHAR LIMIT=26] --> + <!-- Message shown when user enters wrong PIN [CHAR LIMIT=48] --> <string name="kg_wrong_pin_try_again">Wrong PIN. Try again.</string> - <!-- Message shown when user enters wrong PIN/password/pattern below the main message, for ex: "Wrong PIN. Try again" in line 1 and the following text in line 2. [CHAR LIMIT=52] --> + <!-- Message shown when user enters wrong PIN/password/pattern below the main message, for ex: "Wrong PIN. Try again" in line 1 and the following text in line 2. [CHAR LIMIT=70] --> <string name="kg_wrong_input_try_fp_suggestion">Or unlock with fingerprint</string> - <!-- Message shown when user fingerprint is not recognized [CHAR LIMIT=26] --> + <!-- Message shown when user fingerprint is not recognized [CHAR LIMIT=48] --> <string name="kg_fp_not_recognized">Fingerprint not recognized</string> - <!-- Message shown when we want the users to try biometric auth again or use pin/pattern/password [CHAR LIMIT=26] --> + <!-- Message shown when we want the users to try biometric auth again or use pin/pattern/password [CHAR LIMIT=48] --> <string name="bouncer_face_not_recognized">Face not recognized</string> - <!-- Message shown when we want the users to try biometric auth again or use pin/pattern/password [CHAR LIMIT=52] --> + <!-- Message shown when we want the users to try biometric auth again or use pin/pattern/password [CHAR LIMIT=70] --> <string name="kg_bio_try_again_or_pin">Try again or enter PIN</string> - <!-- Message shown when we want the users to try biometric auth again or use pin/pattern/password [CHAR LIMIT=52] --> + <!-- Message shown when we want the users to try biometric auth again or use pin/pattern/password [CHAR LIMIT=70] --> <string name="kg_bio_try_again_or_password">Try again or enter password</string> - <!-- Message shown when we want the users to try biometric auth again or use pin/pattern/password [CHAR LIMIT=52] --> + <!-- Message shown when we want the users to try biometric auth again or use pin/pattern/password [CHAR LIMIT=70] --> <string name="kg_bio_try_again_or_pattern">Try again or draw pattern</string> - <!-- Message shown when we are on bouncer after temporary lockout of either face or fingerprint [CHAR LIMIT=52] --> + <!-- Message shown when we are on bouncer after temporary lockout of either face or fingerprint [CHAR LIMIT=70] --> <string name="kg_bio_too_many_attempts_pin">PIN is required after too many attempts</string> - <!-- Message shown when we are on bouncer after temporary lockout of either face or fingerprint [CHAR LIMIT=52] --> + <!-- Message shown when we are on bouncer after temporary lockout of either face or fingerprint [CHAR LIMIT=70] --> <string name="kg_bio_too_many_attempts_password">Password is required after too many attempts</string> - <!-- Message shown when we are on bouncer after temporary lockout of either face or fingerprint [CHAR LIMIT=52] --> + <!-- Message shown when we are on bouncer after temporary lockout of either face or fingerprint [CHAR LIMIT=70] --> <string name="kg_bio_too_many_attempts_pattern">Pattern is required after too many attempts</string> - <!-- Instructions when the user can unlock with PIN/password/pattern or fingerprint from bouncer. [CHAR LIMIT=26] --> + <!-- Instructions when the user can unlock with PIN/password/pattern or fingerprint from bouncer. [CHAR LIMIT=48] --> <string name="kg_unlock_with_pin_or_fp">Unlock with PIN or fingerprint</string> - <!-- Instructions when the user can unlock with PIN/password/pattern or fingerprint from bouncer. [CHAR LIMIT=26] --> + <!-- Instructions when the user can unlock with PIN/password/pattern or fingerprint from bouncer. [CHAR LIMIT=48] --> <string name="kg_unlock_with_password_or_fp">Unlock with password or fingerprint</string> - <!-- Instructions when the user can unlock with PIN/password/pattern or fingerprint from bouncer. [CHAR LIMIT=26] --> + <!-- Instructions when the user can unlock with PIN/password/pattern or fingerprint from bouncer. [CHAR LIMIT=48] --> <string name="kg_unlock_with_pattern_or_fp">Unlock with pattern or fingerprint</string> - <!-- Message shown when we are on bouncer after Device admin requested lockdown. [CHAR LIMIT=52] --> + <!-- Message shown when we are on bouncer after Device admin requested lockdown. [CHAR LIMIT=70] --> <string name="kg_prompt_after_dpm_lock">For added security, device was locked by work policy</string> - <!-- Message shown for pin/pattern/password when we are on bouncer after user triggered lockdown. [CHAR LIMIT=52] --> + <!-- Message shown for pin/pattern/password when we are on bouncer after user triggered lockdown. [CHAR LIMIT=70] --> <string name="kg_prompt_after_user_lockdown_pin">PIN is required after lockdown</string> - <!-- Message shown for pin/pattern/password when we are on bouncer after user triggered lockdown. [CHAR LIMIT=52] --> + <!-- Message shown for pin/pattern/password when we are on bouncer after user triggered lockdown. [CHAR LIMIT=70] --> <string name="kg_prompt_after_user_lockdown_password">Password is required after lockdown</string> - <!-- Message shown for pin/pattern/password when we are on bouncer after user triggered lockdown. [CHAR LIMIT=52] --> + <!-- Message shown for pin/pattern/password when we are on bouncer after user triggered lockdown. [CHAR LIMIT=70] --> <string name="kg_prompt_after_user_lockdown_pattern">Pattern is required after lockdown</string> - <!-- Message shown to prepare for an unattended update (OTA). Also known as an over-the-air (OTA) update. [CHAR LIMIT=52] --> + <!-- Message shown to prepare for an unattended update (OTA). Also known as an over-the-air (OTA) update. [CHAR LIMIT=70] --> <string name="kg_prompt_unattended_update">Update will install during inactive hours</string> - <!-- Message shown when primary authentication hasn't been used for some time. [CHAR LIMIT=52] --> + <!-- Message shown when primary authentication hasn't been used for some time. [CHAR LIMIT=70] --> <string name="kg_prompt_pin_auth_timeout">Added security required. PIN not used for a while.</string> - <!-- Message shown when primary authentication hasn't been used for some time. [CHAR LIMIT=52] --> + <!-- Message shown when primary authentication hasn't been used for some time. [CHAR LIMIT=70] --> <string name="kg_prompt_password_auth_timeout">Added security required. Password not used for a while.</string> - <!-- Message shown when primary authentication hasn't been used for some time. [CHAR LIMIT=52] --> + <!-- Message shown when primary authentication hasn't been used for some time. [CHAR LIMIT=76] --> <string name="kg_prompt_pattern_auth_timeout">Added security required. Pattern not used for a while.</string> - <!-- Message shown when device hasn't been unlocked for a while. [CHAR LIMIT=52] --> + <!-- Message shown when device hasn't been unlocked for a while. [CHAR LIMIT=82] --> <string name="kg_prompt_auth_timeout">Added security required. Device wasn\u2019t unlocked for a while.</string> - <!-- Message shown when face unlock is not available after too many failed face authentication attempts. [CHAR LIMIT=52] --> + <!-- Message shown when face unlock is not available after too many failed face authentication attempts. [CHAR LIMIT=70] --> <string name="kg_face_locked_out">Can\u2019t unlock with face. Too many attempts.</string> - <!-- Message shown when fingerprint unlock isn't available after too many failed fingerprint authentication attempts. [CHAR LIMIT=52] --> + <!-- Message shown when fingerprint unlock isn't available after too many failed fingerprint authentication attempts. [CHAR LIMIT=75] --> <string name="kg_fp_locked_out">Can\u2019t unlock with fingerprint. Too many attempts.</string> - <!-- Message shown when Trust Agent is disabled. [CHAR LIMIT=52] --> + <!-- Message shown when Trust Agent is disabled. [CHAR LIMIT=70] --> <string name="kg_trust_agent_disabled">Trust agent is unavailable</string> - <!-- Message shown when primary auth is locked out after too many attempts [CHAR LIMIT=52] --> + <!-- Message shown when primary auth is locked out after too many attempts [CHAR LIMIT=70] --> <string name="kg_primary_auth_locked_out_pin">Too many attempts with incorrect PIN</string> - <!-- Message shown when primary auth is locked out after too many attempts [CHAR LIMIT=52] --> + <!-- Message shown when primary auth is locked out after too many attempts [CHAR LIMIT=70] --> <string name="kg_primary_auth_locked_out_pattern">Too many attempts with incorrect pattern</string> - <!-- Message shown when primary auth is locked out after too many attempts [CHAR LIMIT=52] --> + <!-- Message shown when primary auth is locked out after too many attempts [CHAR LIMIT=70] --> <string name="kg_primary_auth_locked_out_password">Too many attempts with incorrect password</string> - <!-- Countdown message shown after too many failed unlock attempts [CHAR LIMIT=26]--> + <!-- Countdown message shown after too many failed unlock attempts [CHAR LIMIT=48]--> <string name="kg_too_many_failed_attempts_countdown">{count, plural, =1 {Try again in # second.} other {Try again in # seconds.} @@ -296,13 +296,13 @@ <!-- Description of airplane mode --> <string name="airplane_mode">Airplane mode</string> - <!-- An explanation text that the pattern needs to be solved since the device has just been restarted. [CHAR LIMIT=52] --> + <!-- An explanation text that the pattern needs to be solved since the device has just been restarted. [CHAR LIMIT=70] --> <string name="kg_prompt_reason_restart_pattern">Pattern is required after device restarts</string> - <!-- An explanation text that the pin needs to be entered since the device has just been restarted. [CHAR LIMIT=52] --> + <!-- An explanation text that the pin needs to be entered since the device has just been restarted. [CHAR LIMIT=70] --> <string name="kg_prompt_reason_restart_pin">PIN is required after device restarts</string> - <!-- An explanation text that the password needs to be entered since the device has just been restarted. [CHAR LIMIT=52] --> + <!-- An explanation text that the password needs to be entered since the device has just been restarted. [CHAR LIMIT=70] --> <string name="kg_prompt_reason_restart_password">Password is required after device restarts</string> <!-- An explanation text that the pattern needs to be solved since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] --> diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index c0f7029449a1..e9acf3fa10c8 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -64,20 +64,20 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="bottom" + android:layout_marginHorizontal="@dimen/keyguard_affordance_horizontal_offset" + android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" + android:gravity="bottom" > <com.android.systemui.animation.view.LaunchableImageView android:id="@+id/start_button" android:layout_height="@dimen/keyguard_affordance_fixed_height" android:layout_width="@dimen/keyguard_affordance_fixed_width" - android:layout_gravity="bottom|start" android:scaleType="fitCenter" android:padding="@dimen/keyguard_affordance_fixed_padding" android:tint="?android:attr/textColorPrimary" android:background="@drawable/keyguard_bottom_affordance_bg" android:foreground="@drawable/keyguard_bottom_affordance_selected_border" - android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset" - android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" android:visibility="invisible" /> <FrameLayout @@ -90,7 +90,7 @@ android:id="@+id/keyguard_settings_button" layout="@layout/keyguard_settings_popup_menu" android:layout_width="wrap_content" - android:layout_height="@dimen/keyguard_affordance_fixed_height" + android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="gone" /> @@ -100,14 +100,11 @@ android:id="@+id/end_button" android:layout_height="@dimen/keyguard_affordance_fixed_height" android:layout_width="@dimen/keyguard_affordance_fixed_width" - android:layout_gravity="bottom|end" android:scaleType="fitCenter" android:padding="@dimen/keyguard_affordance_fixed_padding" android:tint="?android:attr/textColorPrimary" android:background="@drawable/keyguard_bottom_affordance_bg" android:foreground="@drawable/keyguard_bottom_affordance_selected_border" - android:layout_marginEnd="@dimen/keyguard_affordance_horizontal_offset" - android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" android:visibility="invisible" /> </LinearLayout> diff --git a/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml index 9d0d783d0113..65ee8b370f9b 100644 --- a/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml +++ b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml @@ -20,7 +20,8 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" - android:layout_height="48dp" + android:layout_height="wrap_content" + android:minHeight="@dimen/keyguard_affordance_fixed_height" android:orientation="horizontal" android:gravity="center_vertical" android:background="@drawable/keyguard_settings_popup_menu_background" diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index f1fca7603571..d7106762d17b 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -152,10 +152,4 @@ <include layout="@layout/keyguard_bottom_area" android:visibility="gone" /> - - <FrameLayout - android:id="@+id/preview_container" - android:layout_width="match_parent" - android:layout_height="match_parent"> - </FrameLayout> </com.android.systemui.shade.NotificationPanelView> diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java index 7971e84769a2..b15378570358 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java @@ -21,7 +21,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; -import android.net.wifi.WifiManager; +import android.os.Trace; import android.telephony.ServiceState; import android.telephony.SubscriptionInfo; import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener; @@ -37,6 +37,7 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository; import com.android.systemui.telephony.TelephonyListenerManager; import java.util.List; @@ -50,7 +51,10 @@ import javax.inject.Inject; * Controller that generates text including the carrier names and/or the status of all the SIM * interfaces in the device. Through a callback, the updates can be retrieved either as a list or * separated by a given separator {@link CharSequence}. + * + * @deprecated use {@link com.android.systemui.statusbar.pipeline.wifi} instead */ +@Deprecated public class CarrierTextManager { private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "CarrierTextController"; @@ -64,7 +68,7 @@ public class CarrierTextManager { private final AtomicBoolean mNetworkSupported = new AtomicBoolean(); @VisibleForTesting protected KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private final WifiManager mWifiManager; + private final WifiRepository mWifiRepository; private final boolean[] mSimErrorState; private final int mSimSlotsNumber; @Nullable // Check for nullability before dispatching @@ -165,7 +169,7 @@ public class CarrierTextManager { CharSequence separator, boolean showAirplaneMode, boolean showMissingSim, - @Nullable WifiManager wifiManager, + WifiRepository wifiRepository, TelephonyManager telephonyManager, TelephonyListenerManager telephonyListenerManager, WakefulnessLifecycle wakefulnessLifecycle, @@ -177,8 +181,7 @@ public class CarrierTextManager { mShowAirplaneMode = showAirplaneMode; mShowMissingSim = showMissingSim; - - mWifiManager = wifiManager; + mWifiRepository = wifiRepository; mTelephonyManager = telephonyManager; mSeparator = separator; mTelephonyListenerManager = telephonyListenerManager; @@ -297,6 +300,7 @@ public class CarrierTextManager { } protected void updateCarrierText() { + Trace.beginSection("CarrierTextManager#updateCarrierText"); boolean allSimsMissing = true; boolean anySimReadyAndInService = false; CharSequence displayText = null; @@ -329,20 +333,20 @@ public class CarrierTextManager { carrierNames[i] = carrierTextForSimState; } if (simState == TelephonyManager.SIM_STATE_READY) { + Trace.beginSection("WFC check"); ServiceState ss = mKeyguardUpdateMonitor.mServiceStates.get(subId); if (ss != null && ss.getDataRegistrationState() == ServiceState.STATE_IN_SERVICE) { // hack for WFC (IWLAN) not turning off immediately once // Wi-Fi is disassociated or disabled if (ss.getRilDataRadioTechnology() != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN - || (mWifiManager != null && mWifiManager.isWifiEnabled() - && mWifiManager.getConnectionInfo() != null - && mWifiManager.getConnectionInfo().getBSSID() != null)) { + || mWifiRepository.isWifiConnectedWithValidSsid()) { if (DEBUG) { Log.d(TAG, "SIM ready and in service: subId=" + subId + ", ss=" + ss); } anySimReadyAndInService = true; } } + Trace.endSection(); } } // Only create "No SIM card" if no cards with CarrierName && no wifi when some sim is READY @@ -406,6 +410,7 @@ public class CarrierTextManager { subsIds, airplaneMode); postToCallback(info); + Trace.endSection(); } @VisibleForTesting @@ -633,7 +638,7 @@ public class CarrierTextManager { public static class Builder { private final Context mContext; private final String mSeparator; - private final WifiManager mWifiManager; + private final WifiRepository mWifiRepository; private final TelephonyManager mTelephonyManager; private final TelephonyListenerManager mTelephonyListenerManager; private final WakefulnessLifecycle mWakefulnessLifecycle; @@ -647,7 +652,7 @@ public class CarrierTextManager { public Builder( Context context, @Main Resources resources, - @Nullable WifiManager wifiManager, + @Nullable WifiRepository wifiRepository, TelephonyManager telephonyManager, TelephonyListenerManager telephonyListenerManager, WakefulnessLifecycle wakefulnessLifecycle, @@ -657,7 +662,7 @@ public class CarrierTextManager { mContext = context; mSeparator = resources.getString( com.android.internal.R.string.kg_text_message_separator); - mWifiManager = wifiManager; + mWifiRepository = wifiRepository; mTelephonyManager = telephonyManager; mTelephonyListenerManager = telephonyListenerManager; mWakefulnessLifecycle = wakefulnessLifecycle; @@ -681,7 +686,7 @@ public class CarrierTextManager { /** Create a CarrierTextManager. */ public CarrierTextManager build() { return new CarrierTextManager( - mContext, mSeparator, mShowAirplaneMode, mShowMissingSim, mWifiManager, + mContext, mSeparator, mShowAirplaneMode, mShowMissingSim, mWifiRepository, mTelephonyManager, mTelephonyListenerManager, mWakefulnessLifecycle, mMainExecutor, mBgExecutor, mKeyguardUpdateMonitor); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 9290220b8698..25d17928351a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -109,7 +109,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final ContentObserver mShowWeatherObserver = new ContentObserver(null) { @Override public void onChange(boolean change) { - setDateWeatherVisibility(); + setWeatherVisibility(); } }; @@ -236,6 +236,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS updateDoubleLineClock(); setDateWeatherVisibility(); + setWeatherVisibility(); mKeyguardUnlockAnimationController.addKeyguardUnlockAnimationListener( mKeyguardUnlockAnimationListener); @@ -266,6 +267,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mStatusArea.removeView(mDateWeatherView); addDateWeatherView(index); } + setDateWeatherVisibility(); + setWeatherVisibility(); } int index = mStatusArea.indexOfChild(mSmartspaceView); if (index >= 0) { @@ -487,16 +490,19 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void setDateWeatherVisibility() { - if (mDateWeatherView != null || mWeatherView != null) { + if (mDateWeatherView != null) { mUiExecutor.execute(() -> { - if (mDateWeatherView != null) { - mDateWeatherView.setVisibility( - clockHasCustomWeatherDataDisplay() ? View.GONE : View.VISIBLE); - } - if (mWeatherView != null) { - mWeatherView.setVisibility( - mSmartspaceController.isWeatherEnabled() ? View.VISIBLE : View.GONE); - } + mDateWeatherView.setVisibility( + clockHasCustomWeatherDataDisplay() ? View.GONE : View.VISIBLE); + }); + } + } + + private void setWeatherVisibility() { + if (mWeatherView != null) { + mUiExecutor.execute(() -> { + mWeatherView.setVisibility( + mSmartspaceController.isWeatherEnabled() ? View.VISIBLE : View.GONE); }); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index c4df836e401f..6f549881d12a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -16,12 +16,37 @@ package com.android.keyguard; +import static androidx.constraintlayout.widget.ConstraintSet.END; +import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; + +import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION; + +import android.animation.Animator; +import android.animation.ValueAnimator; import android.annotation.Nullable; import android.graphics.Rect; +import android.transition.ChangeBounds; +import android.transition.Transition; +import android.transition.TransitionListenerAdapter; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.transition.TransitionValues; import android.util.Slog; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.VisibleForTesting; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; + +import com.android.internal.jank.InteractionJankMonitor; import com.android.keyguard.KeyguardClockSwitch.ClockSize; import com.android.keyguard.logging.KeyguardLogger; +import com.android.systemui.R; +import com.android.systemui.animation.Interpolators; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ClockController; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; @@ -42,6 +67,12 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardStatusViewController"; + /** + * Duration to use for the animator when the keyguard status view alignment changes, and a + * custom clock animation is in use. + */ + private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000; + private static final AnimationProperties CLOCK_ANIMATION_PROPERTIES = new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); @@ -50,8 +81,25 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final ConfigurationController mConfigurationController; private final KeyguardVisibilityHelper mKeyguardVisibilityHelper; + private final FeatureFlags mFeatureFlags; + private final InteractionJankMonitor mInteractionJankMonitor; private final Rect mClipBounds = new Rect(); + private Boolean mStatusViewCentered = true; + + private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener = + new TransitionListenerAdapter() { + @Override + public void onTransitionCancel(Transition transition) { + mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); + } + + @Override + public void onTransitionEnd(Transition transition) { + mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); + } + }; + @Inject public KeyguardStatusViewController( KeyguardStatusView keyguardStatusView, @@ -62,7 +110,9 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV ConfigurationController configurationController, DozeParameters dozeParameters, ScreenOffAnimationController screenOffAnimationController, - KeyguardLogger logger) { + KeyguardLogger logger, + FeatureFlags featureFlags, + InteractionJankMonitor interactionJankMonitor) { super(keyguardStatusView); mKeyguardSliceViewController = keyguardSliceViewController; mKeyguardClockSwitchController = keyguardClockSwitchController; @@ -71,6 +121,8 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController, dozeParameters, screenOffAnimationController, /* animateYPos= */ true, logger.getBuffer()); + mInteractionJankMonitor = interactionJankMonitor; + mFeatureFlags = featureFlags; } @Override @@ -242,9 +294,141 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV } } - /** Gets the current clock controller. */ - @Nullable - public ClockController getClockController() { - return mKeyguardClockSwitchController.getClock(); + /** + * Updates the alignment of the KeyguardStatusView and animates the transition if requested. + */ + public void updateAlignment( + ConstraintLayout notifContainerParent, + boolean splitShadeEnabled, + boolean shouldBeCentered, + boolean animate) { + if (mStatusViewCentered == shouldBeCentered) { + return; + } + + mStatusViewCentered = shouldBeCentered; + if (notifContainerParent == null) { + return; + } + + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(notifContainerParent); + int statusConstraint = shouldBeCentered ? PARENT_ID : R.id.qs_edge_guideline; + constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END); + if (!animate) { + constraintSet.applyTo(notifContainerParent); + return; + } + + mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); + ChangeBounds transition = new ChangeBounds(); + if (splitShadeEnabled) { + // Excluding media from the transition on split-shade, as it doesn't transition + // horizontally properly. + transition.excludeTarget(R.id.status_view_media_container, true); + } + + transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + + ClockController clock = mKeyguardClockSwitchController.getClock(); + boolean customClockAnimation = clock != null + && clock.getConfig().getHasCustomPositionUpdatedAnimation(); + + if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) { + // Find the clock, so we can exclude it from this transition. + FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large); + + // The clock container can sometimes be null. If it is, just fall back to the + // old animation rather than setting up the custom animations. + if (clockContainerView == null || clockContainerView.getChildCount() == 0) { + transition.addListener(mKeyguardStatusAlignmentTransitionListener); + TransitionManager.beginDelayedTransition(notifContainerParent, transition); + } else { + View clockView = clockContainerView.getChildAt(0); + + transition.excludeTarget(clockView, /* exclude= */ true); + + TransitionSet set = new TransitionSet(); + set.addTransition(transition); + + SplitShadeTransitionAdapter adapter = + new SplitShadeTransitionAdapter(mKeyguardClockSwitchController); + + // Use linear here, so the actual clock can pick its own interpolator. + adapter.setInterpolator(Interpolators.LINEAR); + adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION); + adapter.addTarget(clockView); + set.addTransition(adapter); + set.addListener(mKeyguardStatusAlignmentTransitionListener); + TransitionManager.beginDelayedTransition(notifContainerParent, set); + } + } else { + transition.addListener(mKeyguardStatusAlignmentTransitionListener); + TransitionManager.beginDelayedTransition(notifContainerParent, transition); + } + + constraintSet.applyTo(notifContainerParent); + } + + @VisibleForTesting + static class SplitShadeTransitionAdapter extends Transition { + private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds"; + private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS }; + + private final KeyguardClockSwitchController mController; + + @VisibleForTesting + SplitShadeTransitionAdapter(KeyguardClockSwitchController controller) { + mController = controller; + } + + private void captureValues(TransitionValues transitionValues) { + Rect boundsRect = new Rect(); + boundsRect.left = transitionValues.view.getLeft(); + boundsRect.top = transitionValues.view.getTop(); + boundsRect.right = transitionValues.view.getRight(); + boundsRect.bottom = transitionValues.view.getBottom(); + transitionValues.values.put(PROP_BOUNDS, boundsRect); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Nullable + @Override + public Animator createAnimator(ViewGroup sceneRoot, @Nullable TransitionValues startValues, + @Nullable TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); + + Rect from = (Rect) startValues.values.get(PROP_BOUNDS); + Rect to = (Rect) endValues.values.get(PROP_BOUNDS); + + anim.addUpdateListener(animation -> { + ClockController clock = mController.getClock(); + if (clock == null) { + return; + } + + clock.getAnimations().onPositionUpdated(from, to, animation.getAnimatedFraction()); + }); + + return anim; + } + + @Override + public String[] getTransitionProperties() { + return TRANSITION_PROPERTIES; + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index e1bca89091b2..350c4ed084b9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -96,6 +96,7 @@ import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.SensorProperties; +import android.hardware.biometrics.SensorPropertiesInternal; import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorPropertiesInternal; @@ -1278,6 +1279,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) { lockedOutStateChanged = !mFaceLockedOutPermanent; mFaceLockedOutPermanent = true; + if (isFaceClass3()) { + updateFingerprintListeningState(BIOMETRIC_ACTION_STOP); + } } if (isHwUnavailable && cameraPrivacyEnabled) { @@ -1487,8 +1491,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // STRONG_AUTH_REQUIRED_AFTER_LOCKOUT which is the same as mFingerprintLockedOutPermanent; // however the strong auth tracker does not include the temporary lockout // mFingerprintLockedOut. + // Class 3 biometric lockout will lockout ALL biometrics return mStrongAuthTracker.isUnlockingWithBiometricAllowed(isStrongBiometric) - && !mFingerprintLockedOut; + && (!isFingerprintClass3() || !isFingerprintLockedOut()) + && (!isFaceClass3() || !mFaceLockedOutPermanent); } /** @@ -1506,9 +1512,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @NonNull BiometricSourceType biometricSourceType) { switch (biometricSourceType) { case FINGERPRINT: - return isUnlockingWithBiometricAllowed(true); + return isUnlockingWithBiometricAllowed(isFingerprintClass3()); case FACE: - return isUnlockingWithBiometricAllowed(false); + return isUnlockingWithBiometricAllowed(isFaceClass3()); default: return false; } @@ -2473,7 +2479,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } private void updateFaceEnrolled(int userId) { - Boolean isFaceEnrolled = mFaceManager != null && !mFaceSensorProperties.isEmpty() + final Boolean isFaceEnrolled = isFaceSupported() && mBiometricEnabledForUser.get(userId) && mAuthController.isFaceAuthEnrolled(userId); if (mIsFaceEnrolled != isFaceEnrolled) { @@ -2482,10 +2488,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mIsFaceEnrolled = isFaceEnrolled; } - public boolean isFaceSupported() { + private boolean isFaceSupported() { return mFaceManager != null && !mFaceSensorProperties.isEmpty(); } + private boolean isFingerprintSupported() { + return mFpm != null && !mFingerprintSensorProperties.isEmpty(); + } + /** * @return true if there's at least one udfps enrolled for the current user. */ @@ -2792,10 +2802,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || !mLockPatternUtils.isSecure(user); // Don't trigger active unlock if fp is locked out - final boolean fpLockedOut = mFingerprintLockedOut || mFingerprintLockedOutPermanent; + final boolean fpLockedOut = isFingerprintLockedOut(); // Don't trigger active unlock if primary auth is required - final boolean primaryAuthRequired = !isUnlockingWithBiometricAllowed(true); + final boolean primaryAuthRequired = !isUnlockingWithTrustAgentAllowed(); final boolean shouldTriggerActiveUnlock = (mAuthInterruptActive || triggerActiveUnlockForAssistant || awakeKeyguard) @@ -2857,7 +2867,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || mGoingToSleep || shouldListenForFingerprintAssistant || (mKeyguardOccluded && mIsDreaming) - || (mKeyguardOccluded && userDoesNotHaveTrust + || (mKeyguardOccluded && userDoesNotHaveTrust && mKeyguardShowing && (mOccludingAppRequestingFp || isUdfps || mAlternateBouncerShowing)); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an @@ -2949,7 +2959,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // allow face detection to happen even if stronger auth is required. When face is detected, // we show the bouncer. However, if the user manually locked down the device themselves, // never attempt to detect face. - final boolean supportsDetect = !mFaceSensorProperties.isEmpty() + final boolean supportsDetect = isFaceSupported() && mFaceSensorProperties.get(0).supportsFaceDetection && canBypass && !mPrimaryBouncerIsOrWillBeShowing && !isUserInLockdown(user); @@ -3104,7 +3114,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab : WAKE_REASON_UNKNOWN ).toFaceAuthenticateOptions(); // This would need to be updated for multi-sensor devices - final boolean supportsFaceDetection = !mFaceSensorProperties.isEmpty() + final boolean supportsFaceDetection = isFaceSupported() && mFaceSensorProperties.get(0).supportsFaceDetection; if (!isUnlockingWithBiometricAllowed(FACE)) { final boolean udfpsFingerprintAuthRunning = isUdfpsSupported() @@ -3166,21 +3176,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * @return {@code true} if possible. */ public boolean isUnlockingWithNonStrongBiometricsPossible(int userId) { - // This assumes that there is at most one face and at most one fingerprint sensor - return (mFaceManager != null && !mFaceSensorProperties.isEmpty() - && (mFaceSensorProperties.get(0).sensorStrength != SensorProperties.STRENGTH_STRONG) - && isUnlockWithFacePossible(userId)) - || (mFpm != null && !mFingerprintSensorProperties.isEmpty() - && (mFingerprintSensorProperties.get(0).sensorStrength - != SensorProperties.STRENGTH_STRONG) && isUnlockWithFingerprintPossible(userId)); + return (!isFaceClass3() && isUnlockWithFacePossible(userId)) + || (isFingerprintClass3() && isUnlockWithFingerprintPossible(userId)); } @SuppressLint("MissingPermission") @VisibleForTesting boolean isUnlockWithFingerprintPossible(int userId) { // TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once. - boolean newFpEnrolled = mFpm != null - && !mFingerprintSensorProperties.isEmpty() + boolean newFpEnrolled = isFingerprintSupported() && !isFingerprintDisabled(userId) && mFpm.hasEnrolledTemplates(userId); Boolean oldFpEnrolled = mIsUnlockWithFingerprintPossible.getOrDefault(userId, false); if (oldFpEnrolled != newFpEnrolled) { @@ -3330,12 +3334,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Immediately stop previous biometric listening states. // Resetting lockout states updates the biometric listening states. - if (mFaceManager != null && !mFaceSensorProperties.isEmpty()) { + if (isFaceSupported()) { stopListeningForFace(FACE_AUTH_UPDATED_USER_SWITCHING); handleFaceLockoutReset(mFaceManager.getLockoutModeForUser( mFaceSensorProperties.get(0).sensorId, userId)); } - if (mFpm != null && !mFingerprintSensorProperties.isEmpty()) { + if (isFingerprintSupported()) { stopListeningForFingerprint(); handleFingerprintLockoutReset(mFpm.getLockoutModeForUser( mFingerprintSensorProperties.get(0).sensorId, userId)); @@ -4071,6 +4075,22 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return BIOMETRIC_LOCKOUT_RESET_DELAY_MS; } + @VisibleForTesting + protected boolean isFingerprintClass3() { + // This assumes that there is at most one fingerprint sensor property + return isFingerprintSupported() && isClass3Biometric(mFingerprintSensorProperties.get(0)); + } + + @VisibleForTesting + protected boolean isFaceClass3() { + // This assumes that there is at most one face sensor property + return isFaceSupported() && isClass3Biometric(mFaceSensorProperties.get(0)); + } + + private boolean isClass3Biometric(SensorPropertiesInternal sensorProperties) { + return sensorProperties.sensorStrength == SensorProperties.STRENGTH_STRONG; + } + /** * Unregister all listeners. */ @@ -4122,11 +4142,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int subId : mServiceStates.keySet()) { pw.println(" " + subId + "=" + mServiceStates.get(subId)); } - if (mFpm != null && !mFingerprintSensorProperties.isEmpty()) { + if (isFingerprintSupported()) { final int userId = mUserTracker.getUserId(); final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId); BiometricAuthenticated fingerprint = mUserFingerprintAuthenticated.get(userId); pw.println(" Fingerprint state (user=" + userId + ")"); + pw.println(" isFingerprintClass3=" + isFingerprintClass3()); pw.println(" areAllFpAuthenticatorsRegistered=" + mAuthController.areAllFingerprintAuthenticatorsRegistered()); pw.println(" allowed=" @@ -4184,11 +4205,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintListenBuffer.toList() ).printTableData(pw); } - if (mFaceManager != null && !mFaceSensorProperties.isEmpty()) { + if (isFaceSupported()) { final int userId = mUserTracker.getUserId(); final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId); BiometricAuthenticated face = mUserFaceAuthenticated.get(userId); pw.println(" Face authentication state (user=" + userId + ")"); + pw.println(" isFaceClass3=" + isFaceClass3()); pw.println(" allowed=" + (face != null && isUnlockingWithBiometricAllowed(face.mIsStrongBiometric))); pw.println(" auth'd=" diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 235a8bca6d1e..5f2afe8f755d 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -294,6 +294,11 @@ public class LockIconViewController extends ViewController<LockIconView> impleme final CharSequence prevContentDescription = mView.getContentDescription(); if (mShowLockIcon) { + if (wasShowingFpIcon) { + // fp icon was shown by UdfpsView, and now we still want to animate the transition + // in this drawable + mView.updateIcon(ICON_FINGERPRINT, false); + } mView.updateIcon(ICON_LOCK, false); mView.setContentDescription(mLockedLabel); mView.setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/ChooserPinMigration.kt b/packages/SystemUI/src/com/android/systemui/ChooserPinMigration.kt deleted file mode 100644 index 2f03259766c0..000000000000 --- a/packages/SystemUI/src/com/android/systemui/ChooserPinMigration.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui - -import android.content.ComponentName -import android.content.Context -import android.content.Context.MODE_PRIVATE -import android.content.Intent -import android.content.SharedPreferences -import android.os.Bundle -import android.os.Environment -import android.os.storage.StorageManager -import android.util.Log -import androidx.core.util.Supplier -import com.android.internal.R -import com.android.systemui.broadcast.BroadcastSender -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import java.io.File -import javax.inject.Inject - -/** - * Performs a migration of pinned targets to the unbundled chooser if legacy data exists. - * - * Sends an explicit broadcast with the contents of the legacy pin preferences. The broadcast is - * protected by the RECEIVE_CHOOSER_PIN_MIGRATION permission. This class requires the - * ADD_CHOOSER_PINS permission in order to be able to send this broadcast. - */ -class ChooserPinMigration -@Inject -constructor( - private val context: Context, - private val featureFlags: FeatureFlags, - private val broadcastSender: BroadcastSender, - legacyPinPrefsFileSupplier: LegacyPinPrefsFileSupplier, -) : CoreStartable { - - private val legacyPinPrefsFile = legacyPinPrefsFileSupplier.get() - private val chooserComponent = - ComponentName.unflattenFromString( - context.resources.getString(R.string.config_chooserActivity) - ) - - override fun start() { - if (migrationIsRequired()) { - doMigration() - } - } - - private fun migrationIsRequired(): Boolean { - return featureFlags.isEnabled(Flags.CHOOSER_MIGRATION_ENABLED) && - legacyPinPrefsFile.exists() && - chooserComponent?.packageName != null - } - - private fun doMigration() { - Log.i(TAG, "Beginning migration") - - val legacyPinPrefs = context.getSharedPreferences(legacyPinPrefsFile, MODE_PRIVATE) - - if (legacyPinPrefs.all.isEmpty()) { - Log.i(TAG, "No data to migrate, deleting legacy file") - } else { - sendSharedPreferences(legacyPinPrefs) - Log.i(TAG, "Legacy data sent, deleting legacy preferences") - - val legacyPinPrefsEditor = legacyPinPrefs.edit() - legacyPinPrefsEditor.clear() - if (!legacyPinPrefsEditor.commit()) { - Log.e(TAG, "Failed to delete legacy preferences") - return - } - } - - if (!legacyPinPrefsFile.delete()) { - Log.e(TAG, "Legacy preferences deleted, but failed to delete legacy preferences file") - return - } - - Log.i(TAG, "Legacy preference deletion complete") - } - - private fun sendSharedPreferences(sharedPreferences: SharedPreferences) { - val bundle = Bundle() - - sharedPreferences.all.entries.forEach { (key, value) -> - when (value) { - is Boolean -> bundle.putBoolean(key, value) - else -> Log.e(TAG, "Unsupported preference type for $key: ${value?.javaClass}") - } - } - - sendBundle(bundle) - } - - private fun sendBundle(bundle: Bundle) { - val intent = - Intent().apply { - `package` = chooserComponent?.packageName!! - action = BROADCAST_ACTION - putExtras(bundle) - } - broadcastSender.sendBroadcast(intent, BROADCAST_PERMISSION) - } - - companion object { - private const val TAG = "PinnedShareTargetMigration" - private const val BROADCAST_ACTION = "android.intent.action.CHOOSER_PIN_MIGRATION" - private const val BROADCAST_PERMISSION = "android.permission.RECEIVE_CHOOSER_PIN_MIGRATION" - - class LegacyPinPrefsFileSupplier @Inject constructor(private val context: Context) : - Supplier<File> { - - override fun get(): File { - val packageDirectory = - Environment.getDataUserCePackageDirectory( - StorageManager.UUID_PRIVATE_INTERNAL, - context.userId, - context.packageName, - ) - val sharedPrefsDirectory = File(packageDirectory, "shared_prefs") - return File(sharedPrefsDirectory, "chooser_pin_settings.xml") - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index 179eb391af4e..a3e7d71a92f6 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -35,6 +35,7 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.Utils import com.android.systemui.animation.Interpolators +import com.android.systemui.biometrics.AuthController import com.android.systemui.log.ScreenDecorationsLogger import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.util.asIndenting @@ -52,6 +53,7 @@ class FaceScanningOverlay( val keyguardUpdateMonitor: KeyguardUpdateMonitor, val mainExecutor: Executor, val logger: ScreenDecorationsLogger, + val authController: AuthController, ) : ScreenDecorations.DisplayCutoutView(context, pos) { private var showScanningAnim = false private val rimPaint = Paint() @@ -102,7 +104,9 @@ class FaceScanningOverlay( } override fun enableShowProtection(show: Boolean) { - val showScanningAnimNow = keyguardUpdateMonitor.isFaceDetectionRunning && show + val animationRequired = + keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing + val showScanningAnimNow = animationRequired && show if (showScanningAnimNow == showScanningAnim) { return } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 92344dbbfe15..09992290a9ad 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -1005,9 +1005,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, * not enrolled sfps. This may be false if called before onAllAuthenticatorsRegistered. */ public boolean isRearFpsSupported() { - for (FingerprintSensorPropertiesInternal prop: mFpProps) { - if (prop.sensorType == TYPE_REAR) { - return true; + if (mFpProps != null) { + for (FingerprintSensorPropertiesInternal prop: mFpProps) { + if (prop.sensorType == TYPE_REAR) { + return true; + } } } return false; diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt index 26fc36dd3c9e..81ed07653f26 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt @@ -19,10 +19,18 @@ package com.android.systemui.common.ui.view import android.util.MathUtils import android.view.MotionEvent -/** Returns the distance from the position of this [MotionEvent] and the given coordinates. */ -fun MotionEvent.distanceFrom( - x: Float, - y: Float, +/** + * Returns the distance from the raw position of this [MotionEvent] and the given coordinates. + * Because this is all expected to be in the coordinate space of the display and not the view, + * applying mutations to the view (such as scaling animations) does not affect the distance + * measured. + * @param xOnDisplay the x coordinate relative to the display + * @param yOnDisplay the y coordinate relative to the display + * @return distance from the raw position of this [MotionEvent] and the given coordinates + */ +fun MotionEvent.rawDistanceFrom( + xOnDisplay: Float, + yOnDisplay: Float, ): Float { - return MathUtils.dist(this.x, this.y, x, y) + return MathUtils.dist(this.rawX, this.rawY, xOnDisplay, yOnDisplay) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index df236e7f9c8f..9bf6b2a5b42b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -17,7 +17,6 @@ package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger -import com.android.systemui.ChooserPinMigration import com.android.systemui.ChooserSelector import com.android.systemui.CoreStartable import com.android.systemui.LatencyTester @@ -76,13 +75,6 @@ abstract class SystemUICoreStartableModule { @ClassKey(AuthController::class) abstract fun bindAuthController(service: AuthController): CoreStartable - /** Inject into ChooserPinMigration. */ - @Binds - @IntoMap - @ClassKey(ChooserPinMigration::class) - @PerUser - abstract fun bindChooserPinMigration(sysui: ChooserPinMigration): CoreStartable - /** Inject into ChooserCoreStartable. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index 88c0c50d09a5..4e62104034ee 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -98,7 +98,8 @@ class FaceScanningProviderFactory @Inject constructor( } fun shouldShowFaceScanningAnim(): Boolean { - return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceDetectionRunning + return canShowFaceScanningAnim() && + (keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing) } } @@ -142,6 +143,7 @@ class FaceScanningOverlayProviderImpl( keyguardUpdateMonitor, mainExecutor, logger, + authController, ) view.id = viewId view.setColor(tintColor) diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index b44499f96927..012c8cfdd115 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -65,7 +65,7 @@ object Flags { val FSI_ON_DND_UPDATE = releasedFlag(259130119, "fsi_on_dnd_update") // TODO(b/254512538): Tracking Bug - val INSTANT_VOICE_REPLY = releasedFlag(111, "instant_voice_reply") + val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply") // TODO(b/254512425): Tracking Bug val NOTIFICATION_MEMORY_MONITOR_ENABLED = @@ -103,6 +103,11 @@ object Flags { val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD = releasedFlag(254647461, "filter_unseen_notifs_on_keyguard") + // TODO(b/277338665): Tracking Bug + @JvmField + val NOTIFICATION_SHELF_REFACTOR = + unreleasedFlag(271161129, "notification_shelf_refactor") + // TODO(b/263414400): Tracking Bug @JvmField val NOTIFICATION_ANIMATE_BIG_PICTURE = @@ -406,7 +411,7 @@ object Flags { val MEDIA_RETAIN_RECOMMENDATIONS = releasedFlag(916, "media_retain_recommendations") // TODO(b/270437894): Tracking Bug - val MEDIA_REMOTE_RESUME = unreleasedFlag(917, "media_remote_resume") + val MEDIA_REMOTE_RESUME = unreleasedFlag(917, "media_remote_resume", teamfood = true) // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") @@ -414,9 +419,6 @@ object Flags { // TODO(b/254512758): Tracking Bug @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple") - // TODO(b/270882464): Tracking Bug - val ENABLE_DOCK_SETUP_V2 = releasedFlag(1005, "enable_dock_setup_v2") - // TODO(b/265045965): Tracking Bug val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot") @@ -616,9 +618,6 @@ object Flags { val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = releasedFlag(1504, "sharesheet_scrollable_image_preview") - // TODO(b/274137694) Tracking Bug - val CHOOSER_MIGRATION_ENABLED = unreleasedFlag(1505, "chooser_migration_enabled") - // 1700 - clipboard @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt index 56e73980079d..0abce82527f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt @@ -92,6 +92,9 @@ interface DeviceEntryFaceAuthRepository { /** Current state of whether face authentication is running. */ val isAuthRunning: Flow<Boolean> + /** Whether bypass is currently enabled */ + val isBypassEnabled: Flow<Boolean> + /** * Trigger face authentication. * @@ -166,7 +169,7 @@ constructor( override val isAuthenticated: Flow<Boolean> get() = _isAuthenticated - private val bypassEnabled: Flow<Boolean> = + override val isBypassEnabled: Flow<Boolean> = keyguardBypassController?.let { conflatedCallbackFlow { val callback = @@ -222,7 +225,7 @@ constructor( // & detection is supported & biometric unlock is not allowed. listOf( canFaceAuthOrDetectRun(), - logAndObserve(bypassEnabled, "bypassEnabled"), + logAndObserve(isBypassEnabled, "isBypassEnabled"), logAndObserve( biometricSettingsRepository.isNonStrongBiometricAllowed.isFalse(), "nonStrongBiometricIsNotAllowed" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt index 779095cd1d1e..5745d6ae077e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt @@ -26,7 +26,7 @@ import androidx.core.animation.CycleInterpolator import androidx.core.animation.ObjectAnimator import com.android.systemui.R import com.android.systemui.animation.Expandable -import com.android.systemui.common.ui.view.distanceFrom +import com.android.systemui.common.ui.view.rawDistanceFrom import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper @@ -41,14 +41,14 @@ class KeyguardQuickAffordanceOnTouchListener( private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() private var longPressAnimator: ViewPropertyAnimator? = null - private val down: PointF by lazy { PointF() } + private val downDisplayCoords: PointF by lazy { PointF() } @SuppressLint("ClickableViewAccessibility") override fun onTouch(v: View, event: MotionEvent): Boolean { return when (event.actionMasked) { MotionEvent.ACTION_DOWN -> if (viewModel.configKey != null) { - down.set(event.x, event.y) + downDisplayCoords.set(event.rawX, event.rawY) if (isUsingAccurateTool(event)) { // For accurate tool types (stylus, mouse, etc.), we don't require a // long-press. @@ -81,7 +81,13 @@ class KeyguardQuickAffordanceOnTouchListener( if (!isUsingAccurateTool(event)) { // Moving too far while performing a long-press gesture cancels that // gesture. - if (event.distanceFrom(down.x, down.y) > ViewConfiguration.getTouchSlop()) { + if ( + event + .rawDistanceFrom( + downDisplayCoords.x, + downDisplayCoords.y, + ) > ViewConfiguration.getTouchSlop() + ) { cancel() } } @@ -94,7 +100,7 @@ class KeyguardQuickAffordanceOnTouchListener( // the pointer performs a click. if ( viewModel.configKey != null && - event.distanceFrom(down.x, down.y) <= + event.rawDistanceFrom(downDisplayCoords.x, downDisplayCoords.y) <= ViewConfiguration.getTouchSlop() && falsingManager?.isFalseTap(FalsingManager.NO_PENALTY) == false ) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt index ad3fb637961b..c54203c97013 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt @@ -21,7 +21,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration import com.android.systemui.animation.view.LaunchableLinearLayout -import com.android.systemui.common.ui.view.distanceFrom +import com.android.systemui.common.ui.view.rawDistanceFrom import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel class KeyguardSettingsButtonOnTouchListener( @@ -29,18 +29,20 @@ class KeyguardSettingsButtonOnTouchListener( private val viewModel: KeyguardSettingsMenuViewModel, ) : View.OnTouchListener { - private val downPosition = PointF() + private val downPositionDisplayCoords = PointF() override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { when (motionEvent.actionMasked) { MotionEvent.ACTION_DOWN -> { view.isPressed = true - downPosition.set(motionEvent.x, motionEvent.y) + downPositionDisplayCoords.set(motionEvent.rawX, motionEvent.rawY) viewModel.onTouchGestureStarted() } MotionEvent.ACTION_UP -> { view.isPressed = false - val distanceMoved = motionEvent.distanceFrom(downPosition.x, downPosition.y) + val distanceMoved = + motionEvent + .rawDistanceFrom(downPositionDisplayCoords.x, downPositionDisplayCoords.y) val isClick = distanceMoved < ViewConfiguration.getTouchSlop() viewModel.onTouchGestureEnded(isClick) if (isClick) { diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index c2c1306d2a32..a765702a95b2 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -18,6 +18,10 @@ package com.android.systemui.power; import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_CONFIRMATION; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_LOW_WARNING; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.SaverManualEnabledReason; + import android.app.Dialog; import android.app.KeyguardManager; import android.app.Notification; @@ -691,7 +695,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { d.setTitle(R.string.battery_saver_confirmation_title); d.setPositiveButton(R.string.battery_saver_confirmation_ok, (dialog, which) -> { - setSaverMode(true, false); + setSaverMode(true, false, SAVER_ENABLED_CONFIRMATION); logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_OK); }); d.setNegativeButton(android.R.string.cancel, (dialog, which) -> @@ -790,8 +794,9 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { return builder; } - private void setSaverMode(boolean mode, boolean needFirstTimeWarning) { - BatterySaverUtils.setPowerSaveMode(mContext, mode, needFirstTimeWarning); + private void setSaverMode(boolean mode, boolean needFirstTimeWarning, + @SaverManualEnabledReason int reason) { + BatterySaverUtils.setPowerSaveMode(mContext, mode, needFirstTimeWarning, reason); } private void startBatterySaverSchedulePage() { @@ -839,7 +844,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { } else if (action.equals(ACTION_START_SAVER)) { logEvent(BatteryWarningEvents .LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION_TURN_ON); - setSaverMode(true, true); + setSaverMode(true, true, SAVER_ENABLED_LOW_WARNING); dismissLowBatteryNotification(); } else if (action.equals(ACTION_SHOW_START_SAVER_CONFIRMATION)) { dismissLowBatteryNotification(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index ce690e239da0..57b479e6899f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -26,8 +26,6 @@ import com.android.systemui.R; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; -import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; -import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.util.leak.GarbageMonitor; import java.util.ArrayList; @@ -35,7 +33,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -public interface QSHost extends PanelInteractor, CustomTileAddedRepository { +public interface QSHost { String TILES_SETTING = Settings.Secure.QS_TILES; int POSITION_AT_END = -1; @@ -75,7 +73,11 @@ public interface QSHost extends PanelInteractor, CustomTileAddedRepository { * @see QSFactory#createTileView */ QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView); - /** Create a {@link QSTile} of a {@code tileSpec} type. */ + /** Create a {@link QSTile} of a {@code tileSpec} type. + * + * This should only be called by classes that need to create one-off instances of tiles. + * Do not use to create {@code custom} tiles without explicitly taking care of its lifecycle. + */ QSTile createTile(String tileSpec); /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt new file mode 100644 index 000000000000..14acb4b3ccf4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import android.content.ComponentName +import android.content.Context +import androidx.annotation.GuardedBy +import com.android.internal.logging.InstanceId +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.plugins.qs.QSTileView +import com.android.systemui.qs.external.TileServiceRequestController +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.shared.TileSpec +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +/** + * Adapter to determine what real class to use for classes that depend on [QSHost]. + * + * * When [Flags.QS_PIPELINE_NEW_HOST] is off, all calls will be routed to [QSTileHost]. + * * When [Flags.QS_PIPELINE_NEW_HOST] is on, calls regarding the current set of tiles will be + * routed to [CurrentTilesInteractor]. Other calls (like [warn]) will still be routed to + * [QSTileHost]. + * + * This routing also includes dumps. + */ +@SysUISingleton +class QSHostAdapter +@Inject +constructor( + private val qsTileHost: QSTileHost, + private val interactor: CurrentTilesInteractor, + private val context: Context, + private val tileServiceRequestControllerBuilder: TileServiceRequestController.Builder, + @Application private val scope: CoroutineScope, + private val featureFlags: FeatureFlags, + dumpManager: DumpManager, +) : QSHost { + + companion object { + private const val TAG = "QSTileHost" + } + + private val useNewHost = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST) + + @GuardedBy("callbacksMap") private val callbacksMap = mutableMapOf<QSHost.Callback, Job>() + + init { + scope.launch { tileServiceRequestControllerBuilder.create(this@QSHostAdapter).init() } + // Redirect dump to the correct host (needed for CTS tests) + dumpManager.registerCriticalDumpable( + TAG, + if (useNewHost) interactor else qsTileHost + ) + } + + override fun getTiles(): Collection<QSTile> { + return if (useNewHost) { + interactor.currentQSTiles + } else { + qsTileHost.getTiles() + } + } + + override fun getSpecs(): List<String> { + return if (useNewHost) { + interactor.currentTilesSpecs.map { it.spec } + } else { + qsTileHost.getSpecs() + } + } + + override fun removeTile(spec: String) { + if (useNewHost) { + interactor.removeTiles(listOf(TileSpec.create(spec))) + } else { + qsTileHost.removeTile(spec) + } + } + + override fun addCallback(callback: QSHost.Callback) { + if (useNewHost) { + val job = + scope.launch { + interactor.currentTiles.collect { callback.onTilesChanged() } + } + synchronized(callbacksMap) { callbacksMap.put(callback, job) } + } else { + qsTileHost.addCallback(callback) + } + } + + override fun removeCallback(callback: QSHost.Callback) { + if (useNewHost) { + synchronized(callbacksMap) { callbacksMap.get(callback)?.cancel() } + } else { + qsTileHost.removeCallback(callback) + } + } + + override fun removeTiles(specs: Collection<String>) { + if (useNewHost) { + interactor.removeTiles(specs.map(TileSpec::create)) + } else { + qsTileHost.removeTiles(specs) + } + } + + override fun removeTileByUser(component: ComponentName) { + if (useNewHost) { + interactor.removeTiles(listOf(TileSpec.create(component))) + } else { + qsTileHost.removeTileByUser(component) + } + } + + override fun addTile(spec: String, position: Int) { + if (useNewHost) { + interactor.addTile(TileSpec.create(spec), position) + } else { + qsTileHost.addTile(spec, position) + } + } + + override fun addTile(component: ComponentName, end: Boolean) { + if (useNewHost) { + interactor.addTile( + TileSpec.create(component), + if (end) POSITION_AT_END else 0 + ) + } else { + qsTileHost.addTile(component, end) + } + } + + override fun changeTilesByUser(previousTiles: List<String>, newTiles: List<String>) { + if (useNewHost) { + interactor.setTiles(newTiles.map(TileSpec::create)) + } else { + qsTileHost.changeTilesByUser(previousTiles, newTiles) + } + } + + override fun warn(message: String?, t: Throwable?) { + qsTileHost.warn(message, t) + } + + override fun getContext(): Context { + return if (useNewHost) { + context + } else { + qsTileHost.context + } + } + + override fun getUserContext(): Context { + return if (useNewHost) { + interactor.userContext.value + } else { + qsTileHost.userContext + } + } + + override fun getUserId(): Int { + return if (useNewHost) { + interactor.userId.value + } else { + qsTileHost.userId + } + } + + override fun getUiEventLogger(): UiEventLogger { + return qsTileHost.uiEventLogger + } + + override fun createTileView( + themedContext: Context?, + tile: QSTile?, + collapsedView: Boolean + ): QSTileView { + return qsTileHost.createTileView(themedContext, tile, collapsedView) + } + + override fun createTile(tileSpec: String): QSTile? { + return qsTileHost.createTile(tileSpec) + } + + override fun addTile(spec: String) { + return addTile(spec, QSHost.POSITION_AT_END) + } + + override fun addTile(tile: ComponentName) { + return addTile(tile, false) + } + + override fun indexOf(tileSpec: String): Int { + return specs.indexOf(tileSpec) + } + + override fun getNewInstanceId(): InstanceId { + return qsTileHost.newInstanceId + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 8bbdeeda356c..0ca897373d13 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -37,8 +37,9 @@ import com.android.systemui.ProtoDumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.nano.SystemUIProtoDump; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.qs.QSFactory; @@ -48,9 +49,10 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.external.CustomTileStatePersister; import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; -import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.nano.QsTileState; +import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; +import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.AutoTileManager; @@ -85,7 +87,8 @@ import javax.inject.Provider; * This class also provides the interface for adding/removing/changing tiles. */ @SysUISingleton -public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable { +public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable, + PanelInteractor, CustomTileAddedRepository { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int MAX_QS_INSTANCE_ID = 1 << 20; @@ -99,7 +102,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P private final ArrayList<String> mTileSpecs = new ArrayList<>(); private final TunerService mTunerService; private final PluginManager mPluginManager; - private final DumpManager mDumpManager; private final QSLogger mQSLogger; private final UiEventLogger mUiEventLogger; private final InstanceIdSequence mInstanceIdSequence; @@ -122,9 +124,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P // This is enforced by only cleaning the flag at the end of a successful run of #onTuningChanged private boolean mTilesListDirty = true; - private final TileServiceRequestController mTileServiceRequestController; private TileLifecycleManager.Factory mTileLifeCycleManagerFactory; + private final FeatureFlags mFeatureFlags; + @Inject public QSTileHost(Context context, QSFactory defaultFactory, @@ -132,35 +135,32 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, - DumpManager dumpManager, Optional<CentralSurfaces> centralSurfacesOptional, QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, - TileServiceRequestController.Builder tileServiceRequestControllerBuilder, TileLifecycleManager.Factory tileLifecycleManagerFactory, - UserFileManager userFileManager + UserFileManager userFileManager, + FeatureFlags featureFlags ) { mContext = context; mUserContext = context; mTunerService = tunerService; mPluginManager = pluginManager; - mDumpManager = dumpManager; mQSLogger = qsLogger; mUiEventLogger = uiEventLogger; mMainExecutor = mainExecutor; - mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this); mTileLifeCycleManagerFactory = tileLifecycleManagerFactory; mUserFileManager = userFileManager; + mFeatureFlags = featureFlags; mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID); mCentralSurfacesOptional = centralSurfacesOptional; mQsFactories.add(defaultFactory); pluginManager.addPluginListener(this, QSFactory.class, true); - mDumpManager.registerDumpable(TAG, this); mUserTracker = userTracker; mSecureSettings = secureSettings; mCustomTileStatePersister = customTileStatePersister; @@ -172,7 +172,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P tunerService.addTunable(this, TILES_SETTING); // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. mAutoTiles = autoTiles.get(); - mTileServiceRequestController.init(); }); } @@ -186,8 +185,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P mAutoTiles.destroy(); mTunerService.removeTunable(this); mPluginManager.removePluginListener(this); - mDumpManager.unregisterDumpable(TAG); - mTileServiceRequestController.destroy(); } @Override @@ -300,6 +297,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P if (!TILES_SETTING.equals(key)) { return; } + // Do not process tiles if the flag is enabled. + if (mFeatureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + return; + } if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt index 964fe7104324..3ddd9f1cfe5f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.dagger import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.qs.QSHost +import com.android.systemui.qs.QSHostAdapter import com.android.systemui.qs.QSTileHost import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedSharedPrefsRepository @@ -31,7 +32,7 @@ import dagger.Provides @Module interface QSHostModule { - @Binds fun provideQsHost(controllerImpl: QSTileHost): QSHost + @Binds fun provideQsHost(controllerImpl: QSHostAdapter): QSHost @Module companion object { @@ -39,7 +40,7 @@ interface QSHostModule { @JvmStatic fun providePanelInteractor( featureFlags: FeatureFlags, - qsHost: QSHost, + qsHost: QSTileHost, panelInteractorImpl: PanelInteractorImpl ): PanelInteractor { return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { @@ -53,7 +54,7 @@ interface QSHostModule { @JvmStatic fun provideCustomTileAddedRepository( featureFlags: FeatureFlags, - qsHost: QSHost, + qsHost: QSTileHost, customTileAddedRepository: CustomTileAddedSharedPrefsRepository ): CustomTileAddedRepository { return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt index 00f0a67dbe22..e212bc4e7f5d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt @@ -22,6 +22,8 @@ import com.android.systemui.log.LogBufferFactory import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractorImpl import com.android.systemui.qs.pipeline.prototyping.PrototypeCoreStartable import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import dagger.Binds @@ -38,6 +40,11 @@ abstract class QSPipelineModule { abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository @Binds + abstract fun bindCurrentTilesInteractor( + impl: CurrentTilesInteractorImpl + ): CurrentTilesInteractor + + @Binds @IntoMap @ClassKey(PrototypeCoreStartable::class) abstract fun providePrototypeCoreStartable(startable: PrototypeCoreStartable): CoreStartable diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt index d254e1b3d0d7..595b29a9dcb8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt @@ -32,6 +32,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -53,6 +54,8 @@ interface TileSpecRepository { * at the end of the list. * * Passing [TileSpec.Invalid] is a noop. + * + * Trying to add a tile beyond the end of the list will add it at the end. */ suspend fun addTile(@UserIdInt userId: Int, tile: TileSpec, position: Int = POSITION_AT_END) @@ -61,7 +64,7 @@ interface TileSpecRepository { * * Passing [TileSpec.Invalid] or a non present tile is a noop. */ - suspend fun removeTile(@UserIdInt userId: Int, tile: TileSpec) + suspend fun removeTiles(@UserIdInt userId: Int, tiles: Collection<TileSpec>) /** * Sets the list of current [tiles] for a given [userId]. @@ -106,6 +109,7 @@ constructor( } .onStart { emit(Unit) } .map { secureSettings.getStringForUser(SETTING, userId) ?: "" } + .distinctUntilChanged() .onEach { logger.logTilesChangedInSettings(it, userId) } .map { parseTileSpecs(it, userId) } .flowOn(backgroundDispatcher) @@ -117,7 +121,7 @@ constructor( } val tilesList = loadTiles(userId).toMutableList() if (tile !in tilesList) { - if (position < 0) { + if (position < 0 || position >= tilesList.size) { tilesList.add(tile) } else { tilesList.add(position, tile) @@ -126,12 +130,12 @@ constructor( } } - override suspend fun removeTile(userId: Int, tile: TileSpec) { - if (tile == TileSpec.Invalid) { + override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) { + if (tiles.all { it == TileSpec.Invalid }) { return } val tilesList = loadTiles(userId).toMutableList() - if (tilesList.remove(tile)) { + if (tilesList.removeAll(tiles)) { storeTiles(userId, tilesList.toList()) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt new file mode 100644 index 000000000000..91c6e8b5fcb6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.domain.interactor + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.UserHandle +import com.android.systemui.Dumpable +import com.android.systemui.ProtoDumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.qs.QSFactory +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.external.CustomTile +import com.android.systemui.qs.external.CustomTileStatePersister +import com.android.systemui.qs.external.TileLifecycleManager +import com.android.systemui.qs.external.TileServiceKey +import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.pipeline.domain.model.TileModel +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.qs.toProto +import com.android.systemui.settings.UserTracker +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.kotlin.pairwise +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Interactor for retrieving the list of current QS tiles, as well as making changes to this list + * + * It is [ProtoDumpable] as it needs to be able to dump state for CTS tests. + */ +interface CurrentTilesInteractor : ProtoDumpable { + /** Current list of tiles with their corresponding spec. */ + val currentTiles: StateFlow<List<TileModel>> + + /** User for the [currentTiles]. */ + val userId: StateFlow<Int> + + /** [Context] corresponding to [userId] */ + val userContext: StateFlow<Context> + + /** List of specs corresponding to the last value of [currentTiles] */ + val currentTilesSpecs: List<TileSpec> + get() = currentTiles.value.map(TileModel::spec) + + /** List of tiles corresponding to the last value of [currentTiles] */ + val currentQSTiles: List<QSTile> + get() = currentTiles.value.map(TileModel::tile) + + /** + * Requests that a tile be added in the list of tiles for the current user. + * + * @see TileSpecRepository.addTile + */ + fun addTile(spec: TileSpec, position: Int = TileSpecRepository.POSITION_AT_END) + + /** + * Requests that tiles be removed from the list of tiles for the current user + * + * If tiles with [TileSpec.CustomTileSpec] are removed, their lifecycle will be terminated and + * marked as removed. + * + * @see TileSpecRepository.removeTiles + */ + fun removeTiles(specs: Collection<TileSpec>) + + /** + * Requests that the list of tiles for the current user is changed to [specs]. + * + * If tiles with [TileSpec.CustomTileSpec] are removed, their lifecycle will be terminated and + * marked as removed. + * + * @see TileSpecRepository.setTiles + */ + fun setTiles(specs: List<TileSpec>) +} + +/** + * This implementation of [CurrentTilesInteractor] will try to re-use existing [QSTile] objects when + * possible, in particular: + * * It will only destroy tiles when they are not part of the list of tiles anymore + * * Platform tiles will be kept between users, with a call to [QSTile.userSwitch] + * * [CustomTile]s will only be destroyed if the user changes. + */ +@SysUISingleton +class CurrentTilesInteractorImpl +@Inject +constructor( + private val tileSpecRepository: TileSpecRepository, + private val userRepository: UserRepository, + private val customTileStatePersister: CustomTileStatePersister, + private val tileFactory: QSFactory, + private val customTileAddedRepository: CustomTileAddedRepository, + private val tileLifecycleManagerFactory: TileLifecycleManager.Factory, + private val userTracker: UserTracker, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val logger: QSPipelineLogger, + featureFlags: FeatureFlags, +) : CurrentTilesInteractor { + + private val _currentSpecsAndTiles: MutableStateFlow<List<TileModel>> = + MutableStateFlow(emptyList()) + + override val currentTiles: StateFlow<List<TileModel>> = _currentSpecsAndTiles.asStateFlow() + + // This variable should only be accessed inside the collect of `startTileCollection`. + private val specsToTiles = mutableMapOf<TileSpec, QSTile>() + + private val currentUser = MutableStateFlow(userTracker.userId) + override val userId = currentUser.asStateFlow() + + private val _userContext = MutableStateFlow(userTracker.userContext) + override val userContext = _userContext.asStateFlow() + + init { + if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + startTileCollection() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun startTileCollection() { + scope.launch { + userRepository.selectedUserInfo + .flatMapLatest { user -> + currentUser.value = user.id + _userContext.value = userTracker.userContext + tileSpecRepository.tilesSpecs(user.id).map { user.id to it } + } + .distinctUntilChanged() + .pairwise(-1 to emptyList()) + .flowOn(backgroundDispatcher) + .collect { (old, new) -> + val newTileList = new.second + val userChanged = old.first != new.first + val newUser = new.first + + // Destroy all tiles that are not in the new set + specsToTiles + .filter { it.key !in newTileList } + .forEach { entry -> + logger.logTileDestroyed( + entry.key, + if (userChanged) { + QSPipelineLogger.TileDestroyedReason + .TILE_NOT_PRESENT_IN_NEW_USER + } else { + QSPipelineLogger.TileDestroyedReason.TILE_REMOVED + } + ) + entry.value.destroy() + } + // MutableMap will keep the insertion order + val newTileMap = mutableMapOf<TileSpec, QSTile>() + + newTileList.forEach { tileSpec -> + if (tileSpec !in newTileMap) { + val newTile = + if (tileSpec in specsToTiles) { + processExistingTile( + tileSpec, + specsToTiles.getValue(tileSpec), + userChanged, + newUser + ) + ?: createTile(tileSpec) + } else { + createTile(tileSpec) + } + if (newTile != null) { + newTileMap[tileSpec] = newTile + } + } + } + + val resolvedSpecs = newTileMap.keys.toList() + specsToTiles.clear() + specsToTiles.putAll(newTileMap) + _currentSpecsAndTiles.value = newTileMap.map { TileModel(it.key, it.value) } + if (resolvedSpecs != newTileList) { + // There were some tiles that couldn't be created. Change the value in the + // repository + launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) } + } + } + } + } + + override fun addTile(spec: TileSpec, position: Int) { + scope.launch { + tileSpecRepository.addTile(userRepository.getSelectedUserInfo().id, spec, position) + } + } + + override fun removeTiles(specs: Collection<TileSpec>) { + val currentSpecsCopy = currentTilesSpecs.toSet() + val user = currentUser.value + // intersect: tiles that are there and are being removed + val toFree = currentSpecsCopy.intersect(specs).filterIsInstance<TileSpec.CustomTileSpec>() + toFree.forEach { onCustomTileRemoved(it.componentName, user) } + if (currentSpecsCopy.intersect(specs).isNotEmpty()) { + // We don't want to do the call to set in case getCurrentTileSpecs is not the most + // up to date for this user. + scope.launch { tileSpecRepository.removeTiles(user, specs) } + } + } + + override fun setTiles(specs: List<TileSpec>) { + val currentSpecsCopy = currentTilesSpecs + val user = currentUser.value + if (currentSpecsCopy != specs) { + // minus: tiles that were there but are not there anymore + val toFree = currentSpecsCopy.minus(specs).filterIsInstance<TileSpec.CustomTileSpec>() + toFree.forEach { onCustomTileRemoved(it.componentName, user) } + scope.launch { tileSpecRepository.setTiles(user, specs) } + } + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("CurrentTileInteractorImpl:") + pw.println("User: ${userId.value}") + currentTiles.value + .map { it.tile } + .filterIsInstance<Dumpable>() + .forEach { it.dump(pw, args) } + } + + override fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) { + val data = + currentTiles.value.map { it.tile.state }.mapNotNull { it.toProto() }.toTypedArray() + systemUIProtoDump.tiles = data + } + + private fun onCustomTileRemoved(componentName: ComponentName, userId: Int) { + val intent = Intent().setComponent(componentName) + val lifecycleManager = tileLifecycleManagerFactory.create(intent, UserHandle.of(userId)) + lifecycleManager.onStopListening() + lifecycleManager.onTileRemoved() + customTileStatePersister.removeState(TileServiceKey(componentName, userId)) + customTileAddedRepository.setTileAdded(componentName, userId, false) + lifecycleManager.flushMessagesAndUnbind() + } + + private suspend fun createTile(spec: TileSpec): QSTile? { + val tile = withContext(mainDispatcher) { tileFactory.createTile(spec.spec) } + if (tile == null) { + logger.logTileNotFoundInFactory(spec) + return null + } else { + tile.tileSpec = spec.spec + return if (!tile.isAvailable) { + logger.logTileDestroyed( + spec, + QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE, + ) + tile.destroy() + null + } else { + logger.logTileCreated(spec) + tile + } + } + } + + private fun processExistingTile( + tileSpec: TileSpec, + qsTile: QSTile, + userChanged: Boolean, + user: Int, + ): QSTile? { + return when { + !qsTile.isAvailable -> { + logger.logTileDestroyed( + tileSpec, + QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE + ) + qsTile.destroy() + null + } + // Tile is in the current list of tiles and available. + // We have a handful of different cases + qsTile !is CustomTile -> { + // The tile is not a custom tile. Make sure they are reset to the correct user + qsTile.removeCallbacks() + if (userChanged) { + qsTile.userSwitch(user) + logger.logTileUserChanged(tileSpec, user) + } + qsTile + } + qsTile.user == user -> { + // The tile is a custom tile for the same user, just return it + qsTile.removeCallbacks() + qsTile + } + else -> { + // The tile is a custom tile and the user has changed. Destroy it + qsTile.destroy() + logger.logTileDestroyed( + tileSpec, + QSPipelineLogger.TileDestroyedReason.CUSTOM_TILE_USER_CHANGED + ) + null + } + } + } +} diff --git a/keystore/java/android/security/GenerateRkpKeyException.java b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/TileModel.kt index a2d65e4e7119..e2381ecd8b97 100644 --- a/keystore/java/android/security/GenerateRkpKeyException.java +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/TileModel.kt @@ -1,11 +1,11 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -14,18 +14,19 @@ * limitations under the License. */ -package android.security; +package com.android.systemui.qs.pipeline.domain.model + +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.pipeline.shared.TileSpec /** - * Thrown on problems in attempting to attest to a key using a remotely provisioned key. - * - * @hide + * Container for a [tile] and its [spec]. The following must be true: + * ``` + * spec.spec == tile.tileSpec + * ``` */ -public class GenerateRkpKeyException extends Exception { - - /** - * Constructs a new {@code GenerateRkpKeyException}. - */ - public GenerateRkpKeyException() { +data class TileModel(val spec: TileSpec, val tile: QSTile) { + init { + check(spec.spec == tile.tileSpec) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt index 69d8248a11f5..89408006c300 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt @@ -93,7 +93,7 @@ constructor( private fun performRemove(args: List<String>, spec: TileSpec) { val user = args.getOrNull(2)?.toInt() ?: userRepository.getSelectedUserInfo().id - scope.launch { tileSpecRepository.removeTile(user, spec) } + scope.launch { tileSpecRepository.removeTiles(user, listOf(spec)) } } override fun help(pw: PrintWriter) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt index c691c2f668ad..af1cd0995a21 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt @@ -66,6 +66,10 @@ sealed class TileSpec private constructor(open val spec: String) { } } + fun create(component: ComponentName): CustomTileSpec { + return CustomTileSpec(CustomTile.toSpec(component), component) + } + private val String.isCustomTileSpec: Boolean get() = startsWith(CustomTile.PREFIX) diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt index 200f7431e906..767ce919d027 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt @@ -73,4 +73,59 @@ constructor( { "Tiles changed in settings for user $int1: $str1" } ) } + + /** Log when a tile is destroyed and its reason for destroying. */ + fun logTileDestroyed(spec: TileSpec, reason: TileDestroyedReason) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.DEBUG, + { + str1 = spec.toString() + str2 = reason.readable + }, + { "Tile $str1 destroyed. Reason: $str2" } + ) + } + + /** Log when a tile is created. */ + fun logTileCreated(spec: TileSpec) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.DEBUG, + { str1 = spec.toString() }, + { "Tile $str1 created" } + ) + } + + /** Ĺog when trying to create a tile, but it's not found in the factory. */ + fun logTileNotFoundInFactory(spec: TileSpec) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.VERBOSE, + { str1 = spec.toString() }, + { "Tile $str1 not found in factory" } + ) + } + + /** Log when the user is changed for a platform tile. */ + fun logTileUserChanged(spec: TileSpec, user: Int) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.VERBOSE, + { + str1 = spec.toString() + int1 = user + }, + { "User changed to $int1 for tile $str1" } + ) + } + + /** Reasons for destroying an existing tile. */ + enum class TileDestroyedReason(val readable: String) { + TILE_REMOVED("Tile removed from current set"), + CUSTOM_TILE_USER_CHANGED("User changed for custom tile"), + NEW_TILE_NOT_AVAILABLE("New tile not available"), + EXISTING_TILE_NOT_AVAILABLE("Existing tile not available"), + TILE_NOT_PRESENT_IN_NEW_USER("Tile not present in new user"), + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 5e65cddeeca2..aedd9762a601 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -22,10 +22,6 @@ import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; -import static androidx.constraintlayout.widget.ConstraintSet.END; -import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; - -import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE; @@ -73,12 +69,6 @@ import android.os.Trace; import android.os.UserManager; import android.os.VibrationEffect; import android.provider.Settings; -import android.transition.ChangeBounds; -import android.transition.Transition; -import android.transition.TransitionListenerAdapter; -import android.transition.TransitionManager; -import android.transition.TransitionSet; -import android.transition.TransitionValues; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; @@ -100,8 +90,6 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; import android.widget.FrameLayout; -import androidx.constraintlayout.widget.ConstraintSet; - import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; @@ -163,8 +151,6 @@ import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.ClockAnimations; -import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.FalsingManager.FalsingTapListener; import com.android.systemui.plugins.qs.QS; @@ -301,11 +287,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1); private static final Rect EMPTY_RECT = new Rect(); /** - * Duration to use for the animator when the keyguard status view alignment changes, and a - * custom clock animation is in use. - */ - private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000; - /** * Whether the Shade should animate to reflect Back gesture progress. * To minimize latency at runtime, we cache this, else we'd be reading it every time * updateQsExpansion() is called... and it's called very often. @@ -552,8 +533,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final KeyguardMediaController mKeyguardMediaController; - private boolean mStatusViewCentered = true; - private final Optional<KeyguardUnfoldTransition> mKeyguardUnfoldTransition; private final Optional<NotificationPanelUnfoldAnimationController> mNotificationPanelUnfoldAnimationController; @@ -684,18 +663,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump step.getTransitionState() == TransitionState.RUNNING; }; - private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener = - new TransitionListenerAdapter() { - @Override - public void onTransitionCancel(Transition transition) { - mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); - } - - @Override - public void onTransitionEnd(Transition transition) { - mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); - } - }; private final ActivityStarter mActivityStarter; @Inject @@ -1323,9 +1290,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump keyguardStatusView = (KeyguardStatusView) mLayoutInflater.inflate( R.layout.keyguard_status_view, mNotificationContainerParent, false); mNotificationContainerParent.addView(keyguardStatusView, statusIndex); - // When it's reinflated, this is centered by default. If it shouldn't be, this will update - // below when resources are updated. - mStatusViewCentered = true; attachSplitShadeMediaPlayerContainer( keyguardStatusView.findViewById(R.id.status_view_media_container)); @@ -1620,68 +1584,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void updateKeyguardStatusViewAlignment(boolean animate) { boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); - if (mStatusViewCentered != shouldBeCentered) { - mStatusViewCentered = shouldBeCentered; - ConstraintSet constraintSet = new ConstraintSet(); - constraintSet.clone(mNotificationContainerParent); - int statusConstraint = shouldBeCentered ? PARENT_ID : R.id.qs_edge_guideline; - constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END); - if (animate) { - mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); - ChangeBounds transition = new ChangeBounds(); - if (mSplitShadeEnabled) { - // Excluding media from the transition on split-shade, as it doesn't transition - // horizontally properly. - transition.excludeTarget(R.id.status_view_media_container, true); - } - - transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - - ClockController clock = mKeyguardStatusViewController.getClockController(); - boolean customClockAnimation = clock != null - && clock.getConfig().getHasCustomPositionUpdatedAnimation(); - - if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) { - // Find the clock, so we can exclude it from this transition. - FrameLayout clockContainerView = - mView.findViewById(R.id.lockscreen_clock_view_large); - - // The clock container can sometimes be null. If it is, just fall back to the - // old animation rather than setting up the custom animations. - if (clockContainerView == null || clockContainerView.getChildCount() == 0) { - transition.addListener(mKeyguardStatusAlignmentTransitionListener); - TransitionManager.beginDelayedTransition( - mNotificationContainerParent, transition); - } else { - View clockView = clockContainerView.getChildAt(0); - - transition.excludeTarget(clockView, /* exclude= */ true); - - TransitionSet set = new TransitionSet(); - set.addTransition(transition); - - SplitShadeTransitionAdapter adapter = - new SplitShadeTransitionAdapter(mKeyguardStatusViewController); - - // Use linear here, so the actual clock can pick its own interpolator. - adapter.setInterpolator(Interpolators.LINEAR); - adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION); - adapter.addTarget(clockView); - set.addTransition(adapter); - set.addListener(mKeyguardStatusAlignmentTransitionListener); - TransitionManager.beginDelayedTransition(mNotificationContainerParent, set); - } - } else { - transition.addListener(mKeyguardStatusAlignmentTransitionListener); - TransitionManager.beginDelayedTransition( - mNotificationContainerParent, transition); - } - } - - constraintSet.applyTo(mNotificationContainerParent); - } - mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(mStatusViewCentered)); + mKeyguardStatusViewController.updateAlignment( + mNotificationContainerParent, mSplitShadeEnabled, shouldBeCentered, animate); + mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered)); } private boolean shouldKeyguardStatusViewBeCentered() { @@ -3335,7 +3240,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ipw.print("mIsGestureNavigation="); ipw.println(mIsGestureNavigation); ipw.print("mOldLayoutDirection="); ipw.println(mOldLayoutDirection); ipw.print("mMinFraction="); ipw.println(mMinFraction); - ipw.print("mStatusViewCentered="); ipw.println(mStatusViewCentered); ipw.print("mSplitShadeFullTransitionDistance="); ipw.println(mSplitShadeFullTransitionDistance); ipw.print("mSplitShadeScrimTransitionDistance="); @@ -5130,65 +5034,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } - static class SplitShadeTransitionAdapter extends Transition { - private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds"; - private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS }; - - private final KeyguardStatusViewController mController; - - SplitShadeTransitionAdapter(KeyguardStatusViewController controller) { - mController = controller; - } - - private void captureValues(TransitionValues transitionValues) { - Rect boundsRect = new Rect(); - boundsRect.left = transitionValues.view.getLeft(); - boundsRect.top = transitionValues.view.getTop(); - boundsRect.right = transitionValues.view.getRight(); - boundsRect.bottom = transitionValues.view.getBottom(); - transitionValues.values.put(PROP_BOUNDS, boundsRect); - } - - @Override - public void captureEndValues(TransitionValues transitionValues) { - captureValues(transitionValues); - } - - @Override - public void captureStartValues(TransitionValues transitionValues) { - captureValues(transitionValues); - } - - @Nullable - @Override - public Animator createAnimator(ViewGroup sceneRoot, @Nullable TransitionValues startValues, - @Nullable TransitionValues endValues) { - if (startValues == null || endValues == null) { - return null; - } - ValueAnimator anim = ValueAnimator.ofFloat(0, 1); - - Rect from = (Rect) startValues.values.get(PROP_BOUNDS); - Rect to = (Rect) endValues.values.get(PROP_BOUNDS); - - anim.addUpdateListener(animation -> { - ClockController clock = mController.getClockController(); - if (clock == null) { - return; - } - - clock.getAnimations().onPositionUpdated(from, to, animation.getAnimatedFraction()); - }); - - return anim; - } - - @Override - public String[] getTransitionProperties() { - return TRANSITION_PROPERTIES; - } - } - private final class HeadsUpNotificationViewControllerImpl implements HeadsUpTouchHelper.HeadsUpNotificationViewController { @Override diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index b31ec3319781..7cb1cbe77539 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -17,6 +17,8 @@ package com.android.systemui.shade; +import static android.view.WindowInsets.Type.ime; + import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS; @@ -463,9 +465,17 @@ public class QuickSettingsController { return (mQs != null ? mQs.getHeader().getHeight() : 0) + mPeekHeight; } + private boolean isRemoteInputActiveWithKeyboardUp() { + //TODO(b/227115380) remove the isVisible(ime()) check once isRemoteInputActive is fixed. + // The check for keyboard visibility is a temporary workaround that allows QS to expand + // even when isRemoteInputActive is mistakenly returning true. + return mRemoteInputManager.isRemoteInputActive() + && mPanelView.getRootWindowInsets().isVisible(ime()); + } + public boolean isExpansionEnabled() { return mExpansionEnabledPolicy && mExpansionEnabledAmbient - && !mRemoteInputManager.isRemoteInputActive(); + && !isRemoteInputActiveWithKeyboardUp(); } public float getTransitioningToFullShadeProgress() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index c84894fc81ee..06f43f1eeaa5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -841,6 +841,19 @@ public final class KeyboardShortcutListSearch { BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(bottomSheet); behavior.setState(BottomSheetBehavior.STATE_EXPANDED); behavior.setSkipCollapsed(true); + behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + if (newState == BottomSheetBehavior.STATE_DRAGGING) { + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + // Do nothing. + } + }); mKeyboardShortcutsBottomSheetDialog.setCanceledOnTouchOutside(true); Window keyboardShortcutsWindow = mKeyboardShortcutsBottomSheetDialog.getWindow(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 765c93ed209b..142689e88b51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1127,13 +1127,7 @@ public class KeyguardIndicationController { final boolean faceAuthUnavailable = biometricSourceType == FACE && msgId == BIOMETRIC_HELP_FACE_NOT_AVAILABLE; - // TODO(b/141025588): refactor to reduce repetition of code/comments - // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong - // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to - // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the - // check of whether non-strong biometric is allowed - if (!mKeyguardUpdateMonitor - .isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) + if (isPrimaryAuthRequired() && !faceAuthUnavailable) { return; } @@ -1234,7 +1228,7 @@ public class KeyguardIndicationController { private void onFaceAuthError(int msgId, String errString) { CharSequence deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage(); mFaceAcquiredMessageDeferral.reset(); - if (shouldSuppressFaceError(msgId, mKeyguardUpdateMonitor)) { + if (shouldSuppressFaceError(msgId)) { mKeyguardLogger.logBiometricMessage("suppressingFaceError", msgId, errString); return; } @@ -1248,7 +1242,7 @@ public class KeyguardIndicationController { } private void onFingerprintAuthError(int msgId, String errString) { - if (shouldSuppressFingerprintError(msgId, mKeyguardUpdateMonitor)) { + if (shouldSuppressFingerprintError(msgId)) { mKeyguardLogger.logBiometricMessage("suppressingFingerprintError", msgId, errString); @@ -1257,31 +1251,19 @@ public class KeyguardIndicationController { } } - private boolean shouldSuppressFingerprintError(int msgId, - KeyguardUpdateMonitor updateMonitor) { - // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong - // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to - // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the - // check of whether non-strong biometric is allowed - return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) - && !isLockoutError(msgId)) + private boolean shouldSuppressFingerprintError(int msgId) { + return ((isPrimaryAuthRequired() && !isLockoutError(msgId)) || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED || msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED); } - private boolean shouldSuppressFaceError(int msgId, KeyguardUpdateMonitor updateMonitor) { - // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong - // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to - // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the - // check of whether non-strong biometric is allowed - return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) - && msgId != FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) + private boolean shouldSuppressFaceError(int msgId) { + return ((isPrimaryAuthRequired() && msgId != FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) || msgId == FaceManager.FACE_ERROR_CANCELED || msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS); } - @Override public void onTrustChanged(int userId) { if (!isCurrentUser(userId)) return; @@ -1355,6 +1337,16 @@ public class KeyguardIndicationController { } } + private boolean isPrimaryAuthRequired() { + // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong + // as long as primary auth, i.e. PIN/pattern/password, is required), so it's ok to + // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the + // check of whether non-strong biometric is allowed since strong biometrics can still be + // used. + return !mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + true /* isStrongBiometric */); + } + protected boolean isPluggedInAndCharging() { return mPowerPluggedIn; } @@ -1431,7 +1423,7 @@ public class KeyguardIndicationController { private boolean canUnlockWithFingerprint() { return mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( - getCurrentUser()); + getCurrentUser()) && mKeyguardUpdateMonitor.isUnlockingWithFingerprintAllowed(); } private void showErrorMessageNowOrLater(String errString, @Nullable String followUpMsg) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java index cb4ae286d5c3..f7d37e6b1058 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java @@ -25,7 +25,6 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; -import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconContainer; @@ -35,7 +34,7 @@ import javax.inject.Inject; * Controller class for {@link NotificationShelf}. */ @NotificationRowScope -public class NotificationShelfController { +public class LegacyNotificationShelfControllerImpl implements NotificationShelfController { private final NotificationShelf mView; private final ActivatableNotificationViewController mActivatableNotificationViewController; private final KeyguardBypassController mKeyguardBypassController; @@ -44,7 +43,7 @@ public class NotificationShelfController { private AmbientState mAmbientState; @Inject - public NotificationShelfController( + public LegacyNotificationShelfControllerImpl( NotificationShelf notificationShelf, ActivatableNotificationViewController activatableNotificationViewController, KeyguardBypassController keyguardBypassController, @@ -79,56 +78,42 @@ public class NotificationShelfController { } } + @Override public NotificationShelf getView() { return mView; } + @Override public boolean canModifyColorOfNotifications() { return mAmbientState.isShadeExpanded() && !(mAmbientState.isOnKeyguard() && mKeyguardBypassController.getBypassEnabled()); } + @Override public NotificationIconContainer getShelfIcons() { return mView.getShelfIcons(); } - public @View.Visibility int getVisibility() { - return mView.getVisibility(); - } - - public void setCollapsedIcons(NotificationIconContainer notificationIcons) { - mView.setCollapsedIcons(notificationIcons); - } - + @Override public void bind(AmbientState ambientState, NotificationStackScrollLayoutController notificationStackScrollLayoutController) { mView.bind(ambientState, notificationStackScrollLayoutController); mAmbientState = ambientState; } - public int getHeight() { - return mView.getHeight(); - } - - public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { - mAmbientState = ambientState; - mView.updateState(algorithmState, ambientState); - } - + @Override public int getIntrinsicHeight() { return mView.getIntrinsicHeight(); } + @Override public void setOnActivatedListener(ActivatableNotificationView.OnActivatedListener listener) { mView.setOnActivatedListener(listener); } + @Override public void setOnClickListener(View.OnClickListener onClickListener) { mView.setOnClickListener(onClickListener); } - public int getNotGoneIndex() { - return mView.getNotGoneIndex(); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 4873c9dae89a..d1c6aef7b306 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -24,6 +24,7 @@ import android.content.res.Resources; import android.graphics.Rect; import android.util.AttributeSet; import android.util.IndentingPrintWriter; +import android.util.Log; import android.util.MathUtils; import android.view.View; import android.view.ViewGroup; @@ -64,8 +65,7 @@ import java.io.PrintWriter; * A notification shelf view that is placed inside the notification scroller. It manages the * overflow icons that don't fit into the regular list anymore. */ -public class NotificationShelf extends ActivatableNotificationView implements - View.OnLayoutChangeListener, StateListener { +public class NotificationShelf extends ActivatableNotificationView implements StateListener { private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag; private static final String TAG = "NotificationShelf"; @@ -78,7 +78,6 @@ public class NotificationShelf extends ActivatableNotificationView implements private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll"); private NotificationIconContainer mShelfIcons; - private int[] mTmp = new int[2]; private boolean mHideBackground; private int mStatusBarHeight; private boolean mEnableNotificationClipping; @@ -87,7 +86,6 @@ public class NotificationShelf extends ActivatableNotificationView implements private int mPaddingBetweenElements; private int mNotGoneIndex; private boolean mHasItemsInStableShelf; - private NotificationIconContainer mCollapsedIcons; private int mScrollFastThreshold; private int mStatusBarState; private boolean mInteractive; @@ -99,6 +97,8 @@ public class NotificationShelf extends ActivatableNotificationView implements private NotificationShelfController mController; private float mActualWidth = -1; private boolean mSensitiveRevealAnimEndabled; + private boolean mShelfRefactorFlagEnabled; + private boolean mCanModifyColorOfNotifications; public NotificationShelf(Context context, AttributeSet attrs) { super(context, attrs); @@ -428,7 +428,7 @@ public class NotificationShelf extends ActivatableNotificationView implements transitionAmount = inShelfAmount; } // We don't want to modify the color if the notification is hun'd - if (isLastChild && mController.canModifyColorOfNotifications()) { + if (isLastChild && canModifyColorOfNotifications()) { if (colorOfViewBeforeLast == NO_COLOR) { colorOfViewBeforeLast = ownColorUntinted; } @@ -493,6 +493,14 @@ public class NotificationShelf extends ActivatableNotificationView implements } } + private boolean canModifyColorOfNotifications() { + if (mShelfRefactorFlagEnabled) { + return mCanModifyColorOfNotifications && mAmbientState.isShadeExpanded(); + } else { + return mController.canModifyColorOfNotifications(); + } + } + private void updateCornerRoundnessOnScroll( ActivatableNotificationView anv, float viewStart, @@ -868,10 +876,6 @@ public class NotificationShelf extends ActivatableNotificationView implements return mShelfIcons.getIconState(icon); } - private float getFullyClosedTranslation() { - return -(getIntrinsicHeight() - mStatusBarHeight) / 2; - } - @Override public boolean hasNoContentHeight() { return true; @@ -893,7 +897,6 @@ public class NotificationShelf extends ActivatableNotificationView implements @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - updateRelativeOffset(); // we always want to clip to our sides, such that nothing can draw outside of these bounds int height = getResources().getDisplayMetrics().heightPixels; @@ -903,13 +906,6 @@ public class NotificationShelf extends ActivatableNotificationView implements } } - private void updateRelativeOffset() { - if (mCollapsedIcons != null) { - mCollapsedIcons.getLocationOnScreen(mTmp); - } - getLocationOnScreen(mTmp); - } - /** * @return the index of the notification at which the shelf visually resides */ @@ -924,19 +920,6 @@ public class NotificationShelf extends ActivatableNotificationView implements } } - /** - * @return whether the shelf has any icons in it when a potential animation has finished, i.e - * if the current state would be applied right now - */ - public boolean hasItemsInStableShelf() { - return mHasItemsInStableShelf; - } - - public void setCollapsedIcons(NotificationIconContainer collapsedIcons) { - mCollapsedIcons = collapsedIcons; - mCollapsedIcons.addOnLayoutChangeListener(this); - } - @Override public void onStateChanged(int newState) { mStatusBarState = newState; @@ -983,20 +966,35 @@ public class NotificationShelf extends ActivatableNotificationView implements } @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - updateRelativeOffset(); - } - - @Override public boolean needsClippingToShelf() { return false; } + private void assertRefactorFlagDisabled() { + if (mShelfRefactorFlagEnabled) { + throw new IllegalStateException( + "Code path not supported when Flags.NOTIFICATION_SHELF_REFACTOR is enabled."); + } + } + + private boolean checkRefactorFlagEnabled() { + if (!mShelfRefactorFlagEnabled) { + Log.wtf(TAG, + "Code path not supported when Flags.NOTIFICATION_SHELF_REFACTOR is disabled."); + } + return mShelfRefactorFlagEnabled; + } + public void setController(NotificationShelfController notificationShelfController) { + assertRefactorFlagDisabled(); mController = notificationShelfController; } + public void setCanModifyColorOfNotifications(boolean canModifyColorOfNotifications) { + if (!checkRefactorFlagEnabled()) return; + mCanModifyColorOfNotifications = canModifyColorOfNotifications; + } + public void setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf) { mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf); } @@ -1009,6 +1007,10 @@ public class NotificationShelf extends ActivatableNotificationView implements mSensitiveRevealAnimEndabled = enabled; } + public void setRefactorFlagEnabled(boolean enabled) { + mShelfRefactorFlagEnabled = enabled; + } + /** * This method resets the OnScroll roundness of a view to 0f * <p> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt new file mode 100644 index 000000000000..bf3d47c4a9ca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import android.view.View +import android.view.View.OnClickListener +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView.OnActivatedListener +import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.phone.NotificationIconContainer + +/** Controller interface for [NotificationShelf]. */ +interface NotificationShelfController { + /** The [NotificationShelf] controlled by this Controller. */ + val view: NotificationShelf + + /** @see ExpandableView.getIntrinsicHeight */ + val intrinsicHeight: Int + + /** Container view for icons displayed in the shelf. */ + val shelfIcons: NotificationIconContainer + + /** Whether or not the shelf can modify the color of notifications in the shade. */ + fun canModifyColorOfNotifications(): Boolean + + /** @see ActivatableNotificationView.setOnActivatedListener */ + fun setOnActivatedListener(listener: OnActivatedListener) + + /** Binds the shelf to the host [NotificationStackScrollLayout], via its Controller. */ + fun bind( + ambientState: AmbientState, + notificationStackScrollLayoutController: NotificationStackScrollLayoutController, + ) + + /** @see View.setOnClickListener */ + fun setOnClickListener(listener: OnClickListener) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/NotificationShelfComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/NotificationShelfComponent.java index af8d6ec727d1..98cd84dde199 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/NotificationShelfComponent.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/NotificationShelfComponent.java @@ -16,8 +16,8 @@ package com.android.systemui.statusbar.notification.row.dagger; +import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl; import com.android.systemui.statusbar.NotificationShelf; -import com.android.systemui.statusbar.NotificationShelfController; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import dagger.Binds; @@ -46,7 +46,8 @@ public interface NotificationShelfComponent { * Creates a NotificationShelfController. */ @NotificationRowScope - NotificationShelfController getNotificationShelfController(); + LegacyNotificationShelfControllerImpl getNotificationShelfController(); + /** * Dagger Module that extracts interesting properties from a NotificationShelf. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt new file mode 100644 index 000000000000..db550c00b4a1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shelf.domain.interactor + +import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** Interactor for the [NotificationShelf] */ +@CentralSurfacesComponent.CentralSurfacesScope +class NotificationShelfInteractor +@Inject +constructor( + private val keyguardRepository: KeyguardRepository, + private val deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository, +) { + /** Is the system in a state where the shelf is just a static display of notification icons? */ + val isShelfStatic: Flow<Boolean> + get() = + combine( + keyguardRepository.isKeyguardShowing, + deviceEntryFaceAuthRepository.isBypassEnabled, + ) { isKeyguardShowing, isBypassEnabled -> + isKeyguardShowing && isBypassEnabled + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt new file mode 100644 index 000000000000..bd531caccc5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shelf.ui.viewbinder + +import android.view.View +import android.view.View.OnAttachStateChangeListener +import android.view.accessibility.AccessibilityManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl +import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.NotificationShelfController +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView +import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController +import com.android.systemui.statusbar.notification.row.ExpandableOutlineViewController +import com.android.systemui.statusbar.notification.row.ExpandableViewController +import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.phone.NotificationIconContainer +import com.android.systemui.statusbar.phone.NotificationTapHelper +import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +/** + * Controller class for [NotificationShelf]. This implementation serves as a temporary wrapper + * around a [NotificationShelfViewBinder], so that external code can continue to depend on the + * [NotificationShelfController] interface. Once the [LegacyNotificationShelfControllerImpl] is + * removed, this class can go away and the ViewBinder can be used directly. + */ +@CentralSurfacesScope +class NotificationShelfViewBinderWrapperControllerImpl +@Inject +constructor( + private val shelf: NotificationShelf, + private val viewModel: NotificationShelfViewModel, + featureFlags: FeatureFlags, + private val notifTapHelperFactory: NotificationTapHelper.Factory, + private val a11yManager: AccessibilityManager, + private val falsingManager: FalsingManager, + private val falsingCollector: FalsingCollector, + private val statusBarStateController: SysuiStatusBarStateController, +) : NotificationShelfController { + + override val view: NotificationShelf + get() = shelf + + init { + shelf.apply { + setRefactorFlagEnabled(featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) + useRoundnessSourceTypes(featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES)) + setSensitiveRevealAnimEndabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM)) + } + } + + fun init() { + NotificationShelfViewBinder.bind(viewModel, shelf) + + ActivatableNotificationViewController( + shelf, + notifTapHelperFactory, + ExpandableOutlineViewController(shelf, ExpandableViewController(shelf)), + a11yManager, + falsingManager, + falsingCollector, + ) + .init() + val onAttachStateListener = + object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + statusBarStateController.addCallback( + shelf, + SysuiStatusBarStateController.RANK_SHELF, + ) + } + + override fun onViewDetachedFromWindow(v: View) { + statusBarStateController.removeCallback(shelf) + } + } + shelf.addOnAttachStateChangeListener(onAttachStateListener) + if (shelf.isAttachedToWindow) { + onAttachStateListener.onViewAttachedToWindow(shelf) + } + } + + override val intrinsicHeight: Int + get() = shelf.intrinsicHeight + + override val shelfIcons: NotificationIconContainer + get() = shelf.shelfIcons + + override fun canModifyColorOfNotifications(): Boolean = unsupported + + override fun setOnActivatedListener(listener: ActivatableNotificationView.OnActivatedListener) { + shelf.setOnActivatedListener(listener) + } + + override fun bind( + ambientState: AmbientState, + notificationStackScrollLayoutController: NotificationStackScrollLayoutController, + ) { + shelf.bind(ambientState, notificationStackScrollLayoutController) + } + + override fun setOnClickListener(listener: View.OnClickListener) { + shelf.setOnClickListener(listener) + } + + private val unsupported: Nothing + get() = error("Code path not supported when Flags.NOTIFICATION_SHELF_REFACTOR is enabled") +} + +/** Binds a [NotificationShelf] to its backend. */ +object NotificationShelfViewBinder { + fun bind(viewModel: NotificationShelfViewModel, shelf: NotificationShelf) { + shelf.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.canModifyColorOfNotifications + .onEach(shelf::setCanModifyColorOfNotifications) + .launchIn(this) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt new file mode 100644 index 000000000000..b84834adf122 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shelf.ui.viewmodel + +import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor +import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** ViewModel for [NotificationShelf]. */ +@CentralSurfacesScope +class NotificationShelfViewModel +@Inject +constructor( + private val interactor: NotificationShelfInteractor, +) { + /** Is the shelf allowed to modify the color of notifications in the host layout? */ + val canModifyColorOfNotifications: Flow<Boolean> + get() = interactor.isShelfStatic.map { static -> !static } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java deleted file mode 100644 index 076e5f1c1ce7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPreviewContainer.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.statusbar.phone; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.WindowInsets; -import android.widget.FrameLayout; - -/** - * A view group which contains the preview of phone/camera and draws a black bar at the bottom as - * the fake navigation bar. - */ -public class KeyguardPreviewContainer extends FrameLayout { - - private Drawable mBlackBarDrawable = new Drawable() { - @Override - public void draw(Canvas canvas) { - canvas.save(); - canvas.clipRect(0, getHeight() - getPaddingBottom(), getWidth(), getHeight()); - canvas.drawColor(Color.BLACK); - canvas.restore(); - } - - @Override - public void setAlpha(int alpha) { - // noop - } - - @Override - public void setColorFilter(ColorFilter colorFilter) { - // noop - } - - @Override - public int getOpacity() { - return android.graphics.PixelFormat.OPAQUE; - } - }; - - public KeyguardPreviewContainer(Context context, AttributeSet attrs) { - super(context, attrs); - setBackground(mBlackBarDrawable); - } - - @Override - public WindowInsets onApplyWindowInsets(WindowInsets insets) { - setPadding(0, 0, 0, insets.getStableInsetBottom()); - return super.onApplyWindowInsets(insets); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java index eb19c0d2ad71..057fa42bd347 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -193,7 +193,6 @@ public class NotificationIconAreaController implements public void setupShelf(NotificationShelfController notificationShelfController) { mShelfIcons = notificationShelfController.getShelfIcons(); - notificationShelfController.setCollapsedIcons(mNotificationIcons); } public void onDensityOrFontScaleChanged(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java index 0929233feb88..5d4addab240a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java @@ -33,6 +33,7 @@ import com.android.systemui.biometrics.AuthRippleView; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.privacy.OngoingPrivacyChip; import com.android.systemui.settings.UserTracker; @@ -44,12 +45,14 @@ import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationsQuickSettingsContainer; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.NotificationShelfController; import com.android.systemui.statusbar.OperatorNameViewController; import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent; +import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.phone.KeyguardBottomAreaView; import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator; @@ -76,6 +79,7 @@ import com.android.systemui.util.settings.SecureSettings; import java.util.concurrent.Executor; import javax.inject.Named; +import javax.inject.Provider; import dagger.Binds; import dagger.Module; @@ -130,16 +134,24 @@ public abstract class StatusBarViewModule { @Provides @CentralSurfacesComponent.CentralSurfacesScope public static NotificationShelfController providesStatusBarWindowView( + FeatureFlags featureFlags, + Provider<NotificationShelfViewBinderWrapperControllerImpl> newImpl, NotificationShelfComponent.Builder notificationShelfComponentBuilder, NotificationShelf notificationShelf) { - NotificationShelfComponent component = notificationShelfComponentBuilder - .notificationShelf(notificationShelf) - .build(); - NotificationShelfController notificationShelfController = - component.getNotificationShelfController(); - notificationShelfController.init(); - - return notificationShelfController; + if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) { + NotificationShelfViewBinderWrapperControllerImpl impl = newImpl.get(); + impl.init(); + return impl; + } else { + NotificationShelfComponent component = notificationShelfComponentBuilder + .notificationShelf(notificationShelf) + .build(); + LegacyNotificationShelfControllerImpl notificationShelfController = + component.getNotificationShelfController(); + notificationShelfController.init(); + + return notificationShelfController; + } } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 08c14e743bb6..f800cf496ca9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -33,6 +33,15 @@ interface WifiRepository { /** Observable for the current wifi network activity. */ val wifiActivity: StateFlow<DataActivityModel> + + /** + * Returns true if the device is currently connected to a wifi network with a valid SSID and + * false otherwise. + */ + fun isWifiConnectedWithValidSsid(): Boolean { + val currentNetwork = wifiNetwork.value + return currentNetwork is WifiNetworkModel.Active && currentNetwork.hasValidSsid() + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index 96ab074c6e56..1a41abf031bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.pipeline.wifi.domain.interactor -import android.net.wifi.WifiManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel @@ -76,7 +75,7 @@ constructor( when { info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint -> info.passpointProviderFriendlyName - info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid + info.hasValidSsid() -> info.ssid else -> null } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt index 0923d7848d8c..4b33c88cea30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.wifi.shared.model +import android.net.wifi.WifiManager.UNKNOWN_SSID import android.telephony.SubscriptionManager import androidx.annotation.VisibleForTesting import com.android.systemui.log.table.Diffable @@ -223,6 +224,11 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { } } + /** Returns true if this network has a valid SSID and false otherwise. */ + fun hasValidSsid(): Boolean { + return ssid != null && ssid != UNKNOWN_SSID + } + override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { if (prevVal !is Active) { logFull(row) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 654ba04eba7a..1e63b2a4b3e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -21,6 +21,8 @@ import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN; import static android.os.BatteryManager.EXTRA_HEALTH; import static android.os.BatteryManager.EXTRA_PRESENT; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS; + import android.annotation.WorkerThread; import android.content.BroadcastReceiver; import android.content.Context; @@ -169,7 +171,8 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC @Override public void setPowerSaveMode(boolean powerSave, View view) { if (powerSave) mPowerSaverStartView.set(new WeakReference<>(view)); - BatterySaverUtils.setPowerSaveMode(mContext, powerSave, /*needFirstTimeWarning*/ true); + BatterySaverUtils.setPowerSaveMode(mContext, powerSave, /*needFirstTimeWarning*/ true, + SAVER_ENABLED_QS); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java index f1269f2b012a..673819b20e4b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java @@ -38,14 +38,14 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import dagger.Lazy; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Objects; import javax.inject.Inject; -import dagger.Lazy; - /** */ @SysUISingleton diff --git a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRule2.java b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRule2.java deleted file mode 100644 index e93e86291535..000000000000 --- a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRule2.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.core.animation; - -import android.os.Looper; -import android.os.SystemClock; -import android.util.AndroidRuntimeException; - -import androidx.annotation.NonNull; - -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -import java.util.ArrayList; -import java.util.List; - -/** - * NOTE: this is a copy of the {@link androidx.core.animation.AnimatorTestRule} which attempts to - * circumvent the problems with {@link androidx.core.animation.AnimationHandler} having a static - * list of callbacks. - * - * TODO(b/275602127): remove this and use the original rule once we have the updated androidx code. - */ -public final class AnimatorTestRule2 implements TestRule { - - class TestAnimationHandler extends AnimationHandler { - TestAnimationHandler() { - super(new TestProvider()); - } - - List<AnimationFrameCallback> animationCallbacks = new ArrayList<>(); - - @Override - void addAnimationFrameCallback(AnimationFrameCallback callback) { - animationCallbacks.add(callback); - callback.doAnimationFrame(getCurrentTime()); - } - - @Override - public void removeCallback(AnimationFrameCallback callback) { - int id = animationCallbacks.indexOf(callback); - if (id >= 0) { - animationCallbacks.set(id, null); - } - } - - void onAnimationFrame(long frameTime) { - for (int i = 0; i < animationCallbacks.size(); i++) { - final AnimationFrameCallback callback = animationCallbacks.get(i); - if (callback == null) { - continue; - } - callback.doAnimationFrame(frameTime); - } - } - - @Override - void autoCancelBasedOn(ObjectAnimator objectAnimator) { - for (int i = animationCallbacks.size() - 1; i >= 0; i--) { - AnimationFrameCallback cb = animationCallbacks.get(i); - if (cb == null) { - continue; - } - if (objectAnimator.shouldAutoCancel(cb)) { - ((Animator) animationCallbacks.get(i)).cancel(); - } - } - } - } - - final TestAnimationHandler mTestHandler; - final long mStartTime; - private long mTotalTimeDelta = 0; - private final Object mLock = new Object(); - - public AnimatorTestRule2() { - mStartTime = SystemClock.uptimeMillis(); - mTestHandler = new TestAnimationHandler(); - } - - @NonNull - @Override - public Statement apply(@NonNull final Statement base, @NonNull Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - AnimationHandler.setTestHandler(mTestHandler); - try { - base.evaluate(); - } finally { - AnimationHandler.setTestHandler(null); - } - } - }; - } - - /** - * Advances the animation clock by the given amount of delta in milliseconds. This call will - * produce an animation frame to all the ongoing animations. This method needs to be - * called on the same thread as {@link Animator#start()}. - * - * @param timeDelta the amount of milliseconds to advance - */ - public void advanceTimeBy(long timeDelta) { - if (Looper.myLooper() == null) { - // Throw an exception - throw new AndroidRuntimeException("AnimationTestRule#advanceTimeBy(long) may only be" - + "called on Looper threads"); - } - synchronized (mLock) { - // Advance time & pulse a frame - mTotalTimeDelta += timeDelta < 0 ? 0 : timeDelta; - } - // produce a frame - mTestHandler.onAnimationFrame(getCurrentTime()); - } - - - /** - * Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a - * different time than the time tracked by {@link SystemClock} This method needs to be called on - * the same thread as {@link Animator#start()}. - */ - public long getCurrentTime() { - if (Looper.myLooper() == null) { - // Throw an exception - throw new AndroidRuntimeException("AnimationTestRule#getCurrentTime() may only be" - + "called on Looper threads"); - } - synchronized (mLock) { - return mStartTime + mTotalTimeDelta; - } - } - - - private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider { - TestProvider() { - } - - @Override - public void onNewCallbackAdded(AnimationHandler.AnimationFrameCallback callback) { - callback.doAnimationFrame(getCurrentTime()); - } - - @Override - public void postFrameCallback() { - } - - @Override - public void setFrameDelay(long delay) { - } - - @Override - public long getFrameDelay() { - return 0; - } - } -} - diff --git a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt index bddd60b5970a..e7738aff6278 100644 --- a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt +++ b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt @@ -30,7 +30,7 @@ import org.junit.runner.RunWith @RunWithLooper(setAsMainLooper = true) class AnimatorTestRuleTest : SysuiTestCase() { - @get:Rule val animatorTestRule = AnimatorTestRule2() + @get:Rule val animatorTestRule = AnimatorTestRule() @Test fun testA() { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java index ecf7e0d46373..5557efa97e3e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java @@ -38,8 +38,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.pm.PackageManager; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; import android.provider.Settings; import android.telephony.ServiceState; import android.telephony.SubscriptionInfo; @@ -52,6 +50,8 @@ import android.text.TextUtils; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository; +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -88,8 +88,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { private static final SubscriptionInfo TEST_SUBSCRIPTION_ROAMING = new SubscriptionInfo(0, "", 0, TEST_CARRIER, TEST_CARRIER, NAME_SOURCE_CARRIER_ID, 0xFFFFFF, "", DATA_ROAMING_ENABLE, null, null, null, null, false, null, ""); - @Mock - private WifiManager mWifiManager; + private FakeWifiRepository mWifiRepository = new FakeWifiRepository(); @Mock private WakefulnessLifecycle mWakefulnessLifecycle; @Mock @@ -121,7 +120,6 @@ public class CarrierTextManagerTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); - mContext.addMockSystemService(WifiManager.class, mWifiManager); mContext.addMockSystemService(PackageManager.class, mPackageManager); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)).thenReturn(true); mContext.addMockSystemService(TelephonyManager.class, mTelephonyManager); @@ -144,7 +142,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { when(mTelephonyManager.getActiveModemCount()).thenReturn(3); mCarrierTextManager = new CarrierTextManager.Builder( - mContext, mContext.getResources(), mWifiManager, + mContext, mContext.getResources(), mWifiRepository, mTelephonyManager, mTelephonyListenerManager, mWakefulnessLifecycle, mMainExecutor, mBgExecutor, mKeyguardUpdateMonitor) .setShowAirplaneMode(true) @@ -364,7 +362,11 @@ public class CarrierTextManagerTest extends SysuiTestCase { when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn( TelephonyManager.SIM_STATE_READY); when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); - mockWifi(); + + assertFalse(mWifiRepository.isWifiConnectedWithValidSsid()); + mWifiRepository.setWifiNetwork( + new WifiNetworkModel.Active(0, false, 0, "", false, false, null)); + assertTrue(mWifiRepository.isWifiConnectedWithValidSsid()); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); ServiceState ss = mock(ServiceState.class); @@ -385,13 +387,6 @@ public class CarrierTextManagerTest extends SysuiTestCase { assertNotEquals(AIRPLANE_MODE_TEXT, captor.getValue().carrierText); } - private void mockWifi() { - when(mWifiManager.isWifiEnabled()).thenReturn(true); - WifiInfo wifiInfo = mock(WifiInfo.class); - when(wifiInfo.getBSSID()).thenReturn(""); - when(mWifiManager.getConnectionInfo()).thenReturn(wifiInfo); - } - @Test public void testCreateInfo_noSubscriptions() { reset(mCarrierTextCallback); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 2f627cb5d9d7..b9f8dd945293 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -48,8 +48,10 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.ClockEvents; +import com.android.systemui.plugins.ClockFaceConfig; import com.android.systemui.plugins.ClockFaceController; import com.android.systemui.plugins.ClockFaceEvents; +import com.android.systemui.plugins.ClockTickRate; import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; @@ -185,6 +187,10 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { when(mClockController.getAnimations()).thenReturn(mClockAnimations); when(mClockRegistry.createCurrentClock()).thenReturn(mClockController); when(mClockEventController.getClock()).thenReturn(mClockController); + when(mSmallClockController.getConfig()) + .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false)); + when(mLargeClockController.getConfig()) + .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false)); mSliceView = new View(getContext()); when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView); @@ -367,6 +373,28 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { } @Test + public void testChangeClockDateWeatherEnabled_SetsDateWeatherViewVisibility() { + ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor = + ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class); + when(mSmartspaceController.isEnabled()).thenReturn(true); + when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true); + when(mSmartspaceController.isWeatherEnabled()).thenReturn(true); + mController.init(); + mExecutor.runAllReady(); + assertEquals(View.VISIBLE, mFakeDateView.getVisibility()); + + when(mSmallClockController.getConfig()) + .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true)); + when(mLargeClockController.getConfig()) + .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true)); + verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture()); + listenerArgumentCaptor.getValue().onCurrentClockChanged(); + + mExecutor.runAllReady(); + assertEquals(View.GONE, mFakeDateView.getVisibility()); + } + + @Test public void testGetClock_nullClock_returnsNull() { when(mClockEventController.getClock()).thenReturn(null); assertNull(mController.getClock()); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index 48f7d924667e..f1ee1080f6c0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -16,17 +16,15 @@ package com.android.keyguard; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; +import com.android.internal.jank.InteractionJankMonitor; import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.ClockController; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -45,26 +43,21 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) public class KeyguardStatusViewControllerTest extends SysuiTestCase { - @Mock - private KeyguardStatusView mKeyguardStatusView; - @Mock - private KeyguardSliceViewController mKeyguardSliceViewController; - @Mock - private KeyguardClockSwitchController mKeyguardClockSwitchController; - @Mock - private KeyguardStateController mKeyguardStateController; - @Mock - private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock - ConfigurationController mConfigurationController; - @Mock - DozeParameters mDozeParameters; - @Mock - ScreenOffAnimationController mScreenOffAnimationController; + @Mock private KeyguardStatusView mKeyguardStatusView; + @Mock private KeyguardSliceViewController mKeyguardSliceViewController; + @Mock private KeyguardClockSwitchController mKeyguardClockSwitchController; + @Mock private KeyguardStateController mKeyguardStateController; + @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock private ConfigurationController mConfigurationController; + @Mock private DozeParameters mDozeParameters; + @Mock private ScreenOffAnimationController mScreenOffAnimationController; + @Mock private KeyguardLogger mKeyguardLogger; + @Mock private KeyguardStatusViewController mControllerMock; + @Mock private FeatureFlags mFeatureFlags; + @Mock private InteractionJankMonitor mInteractionJankMonitor; + @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor; - @Mock - KeyguardLogger mKeyguardLogger; private KeyguardStatusViewController mController; @@ -81,7 +74,9 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { mConfigurationController, mDozeParameters, mScreenOffAnimationController, - mKeyguardLogger); + mKeyguardLogger, + mFeatureFlags, + mInteractionJankMonitor); } @Test @@ -116,12 +111,4 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { configurationListenerArgumentCaptor.getValue().onLocaleListChanged(); verify(mKeyguardClockSwitchController).onLocaleListChanged(); } - - @Test - public void getClock_forwardsToClockSwitch() { - ClockController mockClock = mock(ClockController.class); - when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock); - - assertEquals(mockClock, mController.getClockController()); - } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 08813a7fb48a..3eb9590c0b95 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -20,9 +20,12 @@ import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT; +import static android.hardware.biometrics.SensorProperties.STRENGTH_CONVENIENCE; +import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.face.FaceAuthenticateOptions.AUTHENTICATE_REASON_PRIMARY_BOUNCER_SHOWN; import static android.hardware.face.FaceAuthenticateOptions.AUTHENTICATE_REASON_STARTED_WAKING_UP; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON; +import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL; import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE; import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; @@ -41,6 +44,8 @@ import static com.android.systemui.statusbar.policy.DevicePostureController.DEVI import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertEquals; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -79,7 +84,6 @@ import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; -import android.hardware.biometrics.SensorProperties; import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorProperties; @@ -283,33 +287,13 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Before public void setup() throws RemoteException { MockitoAnnotations.initMocks(this); - - mFaceSensorProperties = - List.of(createFaceSensorProperties(/* supportsFaceDetection = */ false)); - when(mFaceManager.isHardwareDetected()).thenReturn(true); - when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true); - when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties); when(mSessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(mKeyguardInstanceId); - mFingerprintSensorProperties = List.of( - new FingerprintSensorPropertiesInternal(1 /* sensorId */, - FingerprintSensorProperties.STRENGTH_STRONG, - 1 /* maxEnrollmentsPerUser */, - List.of(new ComponentInfoInternal("fingerprintSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, - "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)), - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - false /* resetLockoutRequiresHAT */)); - when(mFingerprintManager.isHardwareDetected()).thenReturn(true); - when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); - when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn( - mFingerprintSensorProperties); when(mUserManager.isUserUnlocked(anyInt())).thenReturn(true); when(mUserManager.isPrimaryUser()).thenReturn(true); when(mStrongAuthTracker.getStub()).thenReturn(mock(IStrongAuthTracker.Stub.class)); when(mStrongAuthTracker - .isUnlockingWithBiometricAllowed(anyBoolean() /* isStrongBiometric */)) + .isUnlockingWithBiometricAllowed(anyBoolean() /* isClass3Biometric */)) .thenReturn(true); when(mTelephonyManager.getServiceStateForSubscriber(anyInt())) .thenReturn(new ServiceState()); @@ -346,20 +330,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { anyInt()); mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); - - ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> faceCaptor = - ArgumentCaptor.forClass(IFaceAuthenticatorsRegisteredCallback.class); - verify(mFaceManager).addAuthenticatorsRegisteredCallback(faceCaptor.capture()); - mFaceAuthenticatorsRegisteredCallback = faceCaptor.getValue(); - mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); - - ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> fingerprintCaptor = - ArgumentCaptor.forClass(IFingerprintAuthenticatorsRegisteredCallback.class); - verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( - fingerprintCaptor.capture()); - mFingerprintAuthenticatorsRegisteredCallback = fingerprintCaptor.getValue(); - mFingerprintAuthenticatorsRegisteredCallback - .onAllAuthenticatorsRegistered(mFingerprintSensorProperties); + captureAuthenticatorsRegisteredCallbacks(); + setupFaceAuth(/* isClass3 */ false); + setupFingerprintAuth(/* isClass3 */ true); verify(mBiometricManager) .registerEnabledOnKeyguardCallback(mBiometricEnabledCallbackArgCaptor.capture()); @@ -381,8 +354,64 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mAuthController.areAllFingerprintAuthenticatorsRegistered()).thenReturn(true); } + private void captureAuthenticatorsRegisteredCallbacks() throws RemoteException { + ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> faceCaptor = + ArgumentCaptor.forClass(IFaceAuthenticatorsRegisteredCallback.class); + verify(mFaceManager).addAuthenticatorsRegisteredCallback(faceCaptor.capture()); + mFaceAuthenticatorsRegisteredCallback = faceCaptor.getValue(); + mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); + + ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> fingerprintCaptor = + ArgumentCaptor.forClass(IFingerprintAuthenticatorsRegisteredCallback.class); + verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( + fingerprintCaptor.capture()); + mFingerprintAuthenticatorsRegisteredCallback = fingerprintCaptor.getValue(); + mFingerprintAuthenticatorsRegisteredCallback + .onAllAuthenticatorsRegistered(mFingerprintSensorProperties); + } + + private void setupFaceAuth(boolean isClass3) throws RemoteException { + when(mFaceManager.isHardwareDetected()).thenReturn(true); + when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true); + mFaceSensorProperties = + List.of(createFaceSensorProperties(/* supportsFaceDetection = */ false, isClass3)); + when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties); + mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); + assertEquals(isClass3, mKeyguardUpdateMonitor.isFaceClass3()); + } + + private void setupFingerprintAuth(boolean isClass3) throws RemoteException { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + mFingerprintSensorProperties = List.of( + createFingerprintSensorPropertiesInternal(TYPE_UDFPS_OPTICAL, isClass3)); + when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn( + mFingerprintSensorProperties); + mFingerprintAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered( + mFingerprintSensorProperties); + assertEquals(isClass3, mKeyguardUpdateMonitor.isFingerprintClass3()); + } + + private FingerprintSensorPropertiesInternal createFingerprintSensorPropertiesInternal( + @FingerprintSensorProperties.SensorType int sensorType, + boolean isClass3) { + final List<ComponentInfoInternal> componentInfo = + List.of(new ComponentInfoInternal("fingerprintSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, + "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */)); + return new FingerprintSensorPropertiesInternal( + FINGERPRINT_SENSOR_ID, + isClass3 ? STRENGTH_STRONG : STRENGTH_CONVENIENCE, + 1 /* maxEnrollmentsPerUser */, + componentInfo, + sensorType, + true /* resetLockoutRequiresHardwareAuthToken */); + } + @NonNull - private FaceSensorPropertiesInternal createFaceSensorProperties(boolean supportsFaceDetection) { + private FaceSensorPropertiesInternal createFaceSensorProperties( + boolean supportsFaceDetection, boolean isClass3) { final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, @@ -391,10 +420,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, "vendor/version/revision" /* softwareVersion */)); - return new FaceSensorPropertiesInternal( - 0 /* id */, - FaceSensorProperties.STRENGTH_STRONG, + FACE_SENSOR_ID /* id */, + isClass3 ? STRENGTH_STRONG : STRENGTH_CONVENIENCE, 1 /* maxTemplatesAllowed */, componentInfo, FaceSensorProperties.TYPE_UNKNOWN, @@ -686,7 +714,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testUnlockingWithFaceAllowed_strongAuthTrackerUnlockingWithBiometricAllowed() { // GIVEN unlocking with biometric is allowed - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); // THEN unlocking with face and fp is allowed Assert.assertTrue(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( @@ -706,12 +734,31 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void testUnlockingWithFaceAllowed_fingerprintLockout() { - // GIVEN unlocking with biometric is allowed - strongAuthNotRequired(); + public void class3FingerprintLockOut_lockOutClass1Face() throws RemoteException { + setupFaceAuth(/* isClass3 */ false); + setupFingerprintAuth(/* isClass3 */ true); + + // GIVEN primary auth is not required by StrongAuthTracker + primaryAuthNotRequiredByStrongAuthTracker(); + + // WHEN fingerprint (class 3) is lock out + fingerprintErrorTemporaryLockOut(); + + // THEN unlocking with face is not allowed + Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FACE)); + } + + @Test + public void class3FingerprintLockOut_lockOutClass3Face() throws RemoteException { + setupFaceAuth(/* isClass3 */ true); + setupFingerprintAuth(/* isClass3 */ true); - // WHEN fingerprint is locked out - fingerprintErrorTemporaryLockedOut(); + // GIVEN primary auth is not required by StrongAuthTracker + primaryAuthNotRequiredByStrongAuthTracker(); + + // WHEN fingerprint (class 3) is lock out + fingerprintErrorTemporaryLockOut(); // THEN unlocking with face is not allowed Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( @@ -719,6 +766,38 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void class3FaceLockOut_lockOutClass3Fingerprint() throws RemoteException { + setupFaceAuth(/* isClass3 */ true); + setupFingerprintAuth(/* isClass3 */ true); + + // GIVEN primary auth is not required by StrongAuthTracker + primaryAuthNotRequiredByStrongAuthTracker(); + + // WHEN face (class 3) is lock out + faceAuthLockOut(); + + // THEN unlocking with fingerprint is not allowed + Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT)); + } + + @Test + public void class1FaceLockOut_doesNotLockOutClass3Fingerprint() throws RemoteException { + setupFaceAuth(/* isClass3 */ false); + setupFingerprintAuth(/* isClass3 */ true); + + // GIVEN primary auth is not required by StrongAuthTracker + primaryAuthNotRequiredByStrongAuthTracker(); + + // WHEN face (class 1) is lock out + faceAuthLockOut(); + + // THEN unlocking with fingerprint is still allowed + Assert.assertTrue(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT)); + } + + @Test public void testUnlockingWithFpAllowed_strongAuthTrackerUnlockingWithBiometricNotAllowed() { // GIVEN unlocking with biometric is not allowed when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); @@ -731,10 +810,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testUnlockingWithFpAllowed_fingerprintLockout() { // GIVEN unlocking with biometric is allowed - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); - // WHEN fingerprint is locked out - fingerprintErrorTemporaryLockedOut(); + // WHEN fingerprint is lock out + fingerprintErrorTemporaryLockOut(); // THEN unlocking with fingerprint is not allowed Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( @@ -757,8 +836,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.onTrustChanged(true, true, getCurrentUser(), 0, null); Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser())); - // WHEN fingerprint is locked out - fingerprintErrorTemporaryLockedOut(); + // WHEN fingerprint is lock out + fingerprintErrorTemporaryLockOut(); // THEN user is NOT considered as "having trust" and bouncer cannot be skipped Assert.assertFalse(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser())); @@ -826,7 +905,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // GIVEN udfps is supported and strong auth required for weak biometrics (face) only givenUdfpsSupported(); - strongAuthRequiredForWeakBiometricOnly(); // this allows fingerprint to run but not face + primaryAuthRequiredForWeakBiometricOnly(); // allows class3 fp to run but not class1 face // WHEN the device wakes up mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); @@ -863,7 +942,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void noFaceDetect_whenStrongAuthRequiredAndBypass_faceDetectionUnsupported() { // GIVEN bypass is enabled, face detection is NOT supported and strong auth is required lockscreenBypassIsAllowed(); - strongAuthRequiredEncrypted(); + primaryAuthRequiredEncrypted(); keyguardIsVisible(); // WHEN the device wakes up @@ -931,6 +1010,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); verifyFingerprintAuthenticateNeverCalled(); // WHEN alternate bouncer is shown + mKeyguardUpdateMonitor.setKeyguardShowing(true, true); mKeyguardUpdateMonitor.setAlternateBouncerShowing(true); // THEN make sure FP listening begins @@ -1011,10 +1091,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testOnFaceAuthenticated_skipsFaceWhenAuthenticated() { - // test whether face will be skipped if authenticated, so the value of isStrongBiometric + // test whether face will be skipped if authenticated, so the value of isClass3Biometric // doesn't matter here mKeyguardUpdateMonitor.onFaceAuthenticated(KeyguardUpdateMonitor.getCurrentUser(), - true /* isStrongBiometric */); + true /* isClass3Biometric */); setKeyguardBouncerVisibility(true); mTestableLooper.processAllMessages(); @@ -1027,7 +1107,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper.processAllMessages(); keyguardIsVisible(); - faceAuthLockedOut(); + faceAuthLockOut(); verify(mLockPatternUtils, never()).requireStrongAuth(anyInt(), anyInt()); } @@ -1050,7 +1130,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper.processAllMessages(); keyguardIsVisible(); - faceAuthLockedOut(); + faceAuthLockOut(); mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, ""); @@ -1060,32 +1140,32 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testGetUserCanSkipBouncer_whenFace() { int user = KeyguardUpdateMonitor.getCurrentUser(); - mKeyguardUpdateMonitor.onFaceAuthenticated(user, true /* isStrongBiometric */); + mKeyguardUpdateMonitor.onFaceAuthenticated(user, true /* isClass3Biometric */); assertThat(mKeyguardUpdateMonitor.getUserCanSkipBouncer(user)).isTrue(); } @Test public void testGetUserCanSkipBouncer_whenFace_nonStrongAndDisallowed() { - when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(false /* isStrongBiometric */)) + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(false /* isClass3Biometric */)) .thenReturn(false); int user = KeyguardUpdateMonitor.getCurrentUser(); - mKeyguardUpdateMonitor.onFaceAuthenticated(user, false /* isStrongBiometric */); + mKeyguardUpdateMonitor.onFaceAuthenticated(user, false /* isClass3Biometric */); assertThat(mKeyguardUpdateMonitor.getUserCanSkipBouncer(user)).isFalse(); } @Test public void testGetUserCanSkipBouncer_whenFingerprint() { int user = KeyguardUpdateMonitor.getCurrentUser(); - mKeyguardUpdateMonitor.onFingerprintAuthenticated(user, true /* isStrongBiometric */); + mKeyguardUpdateMonitor.onFingerprintAuthenticated(user, true /* isClass3Biometric */); assertThat(mKeyguardUpdateMonitor.getUserCanSkipBouncer(user)).isTrue(); } @Test public void testGetUserCanSkipBouncer_whenFingerprint_nonStrongAndDisallowed() { - when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(false /* isStrongBiometric */)) + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(false /* isClass3Biometric */)) .thenReturn(false); int user = KeyguardUpdateMonitor.getCurrentUser(); - mKeyguardUpdateMonitor.onFingerprintAuthenticated(user, false /* isStrongBiometric */); + mKeyguardUpdateMonitor.onFingerprintAuthenticated(user, false /* isClass3Biometric */); assertThat(mKeyguardUpdateMonitor.getUserCanSkipBouncer(user)).isFalse(); } @@ -1126,9 +1206,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @BiometricConstants.LockoutMode int fingerprintLockoutMode, @BiometricConstants.LockoutMode int faceLockoutMode) { final int newUser = 12; - final boolean faceLocked = + final boolean faceLockOut = faceLockoutMode != BiometricConstants.BIOMETRIC_LOCKOUT_NONE; - final boolean fpLocked = + final boolean fpLockOut = fingerprintLockoutMode != BiometricConstants.BIOMETRIC_LOCKOUT_NONE; mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); @@ -1161,8 +1241,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { eq(false), eq(BiometricSourceType.FINGERPRINT)); // THEN locked out states are updated - assertThat(mKeyguardUpdateMonitor.isFingerprintLockedOut()).isEqualTo(fpLocked); - assertThat(mKeyguardUpdateMonitor.isFaceLockedOut()).isEqualTo(faceLocked); + assertThat(mKeyguardUpdateMonitor.isFingerprintLockedOut()).isEqualTo(fpLockOut); + assertThat(mKeyguardUpdateMonitor.isFaceLockedOut()).isEqualTo(faceLockOut); // Fingerprint should be cancelled on lockout if going to lockout state, else // restarted if it's not @@ -1443,7 +1523,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { throws RemoteException { // GIVEN SFPS supported and enrolled final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); - props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + props.add(createFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON, + /* isClass3 */ true)); when(mAuthController.getSfpsProps()).thenReturn(props); when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); @@ -1466,17 +1547,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); } - private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal( - @FingerprintSensorProperties.SensorType int sensorType) { - return new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, - SensorProperties.STRENGTH_STRONG, - 1 /* maxEnrollmentsPerUser */, - new ArrayList<ComponentInfoInternal>(), - sensorType, - true /* resetLockoutRequiresHardwareAuthToken */); - } - @Test public void testShouldNotListenForUdfps_whenTrustEnabled() { // GIVEN a "we should listen for udfps" state @@ -1613,7 +1683,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { keyguardNotGoingAway(); occludingAppRequestsFaceAuth(); currentUserIsPrimary(); - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); biometricsEnabledForCurrentUser(); currentUserDoesNotHaveTrust(); biometricsNotDisabledThroughDevicePolicyManager(); @@ -1622,7 +1692,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); // Fingerprint is locked out. - fingerprintErrorTemporaryLockedOut(); + fingerprintErrorTemporaryLockOut(); assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); } @@ -1634,7 +1704,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { bouncerFullyVisibleAndNotGoingToSleep(); keyguardNotGoingAway(); currentUserIsPrimary(); - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); biometricsEnabledForCurrentUser(); currentUserDoesNotHaveTrust(); biometricsNotDisabledThroughDevicePolicyManager(); @@ -1657,7 +1727,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { bouncerFullyVisibleAndNotGoingToSleep(); keyguardNotGoingAway(); currentUserIsPrimary(); - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); biometricsEnabledForCurrentUser(); currentUserDoesNotHaveTrust(); biometricsNotDisabledThroughDevicePolicyManager(); @@ -1684,7 +1754,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // Face auth should run when the following is true. keyguardNotGoingAway(); bouncerFullyVisibleAndNotGoingToSleep(); - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); biometricsEnabledForCurrentUser(); currentUserDoesNotHaveTrust(); biometricsNotDisabledThroughDevicePolicyManager(); @@ -1940,7 +2010,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); // Face is locked out. - faceAuthLockedOut(); + faceAuthLockOut(); mTestableLooper.processAllMessages(); // This is needed beccause we want to show face locked out error message whenever face auth @@ -2578,7 +2648,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mFingerprintManager).addLockoutResetCallback(fpLockoutResetCallbackCaptor.capture()); // GIVEN device is locked out - fingerprintErrorTemporaryLockedOut(); + fingerprintErrorTemporaryLockOut(); // GIVEN FP detection is running givenDetectFingerprintWithClearingFingerprintManagerInvocations(); @@ -2662,6 +2732,21 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { KeyguardUpdateMonitor.getCurrentUser())).isTrue(); } + @Test + public void testFingerprintListeningStateWhenOccluded() { + when(mAuthController.isUdfpsSupported()).thenReturn(true); + + mKeyguardUpdateMonitor.setKeyguardShowing(false, false); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_BIOMETRIC); + mKeyguardUpdateMonitor.setKeyguardShowing(false, true); + + verifyFingerprintAuthenticateNeverCalled(); + + mKeyguardUpdateMonitor.setKeyguardShowing(true, true); + mKeyguardUpdateMonitor.setAlternateBouncerShowing(true); + + verifyFingerprintAuthenticateCall(); + } private void verifyFingerprintAuthenticateNeverCalled() { verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), any()); @@ -2705,8 +2790,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void supportsFaceDetection() throws RemoteException { + final boolean isClass3 = !mFaceSensorProperties.isEmpty() + && mFaceSensorProperties.get(0).sensorStrength == STRENGTH_STRONG; mFaceSensorProperties = - List.of(createFaceSensorProperties(/* supportsFaceDetection = */ true)); + List.of(createFaceSensorProperties(/* supportsFaceDetection = */ true, isClass3)); mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); } @@ -2734,7 +2821,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } } - private void faceAuthLockedOut() { + private void faceAuthLockOut() { mKeyguardUpdateMonitor.mFaceAuthenticationCallback .onAuthenticationError(FaceManager.FACE_ERROR_LOCKOUT_PERMANENT, ""); } @@ -2767,7 +2854,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.setSwitchingUser(true); } - private void fingerprintErrorTemporaryLockedOut() { + private void fingerprintErrorTemporaryLockOut() { mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT, "Fingerprint locked out"); } @@ -2821,18 +2908,18 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { ); } - private void strongAuthRequiredEncrypted() { + private void primaryAuthRequiredEncrypted() { when(mStrongAuthTracker.getStrongAuthForUser(KeyguardUpdateMonitor.getCurrentUser())) .thenReturn(STRONG_AUTH_REQUIRED_AFTER_BOOT); when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); } - private void strongAuthRequiredForWeakBiometricOnly() { + private void primaryAuthRequiredForWeakBiometricOnly() { when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(eq(true))).thenReturn(true); when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(eq(false))).thenReturn(false); } - private void strongAuthNotRequired() { + private void primaryAuthNotRequiredByStrongAuthTracker() { when(mStrongAuthTracker.getStrongAuthForUser(KeyguardUpdateMonitor.getCurrentUser())) .thenReturn(0); when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); @@ -2917,10 +3004,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void givenDetectFace() throws RemoteException { - // GIVEN bypass is enabled, face detection is supported and strong auth is required + // GIVEN bypass is enabled, face detection is supported and primary auth is required lockscreenBypassIsAllowed(); supportsFaceDetection(); - strongAuthRequiredEncrypted(); + primaryAuthRequiredEncrypted(); keyguardIsVisible(); // fingerprint is NOT running, UDFPS is NOT supported diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/SplitShadeTransitionAdapterTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt index 64fec5bfd4ed..dea2082a2c22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/SplitShadeTransitionAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.shade +package com.android.keyguard import android.animation.Animator import android.testing.AndroidTestingRunner import android.transition.TransitionValues import androidx.test.filters.SmallTest -import com.android.keyguard.KeyguardStatusViewController +import com.android.keyguard.KeyguardStatusViewController.SplitShadeTransitionAdapter import com.android.systemui.SysuiTestCase -import com.android.systemui.shade.NotificationPanelViewController.SplitShadeTransitionAdapter import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -33,14 +32,14 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) class SplitShadeTransitionAdapterTest : SysuiTestCase() { - @Mock private lateinit var keyguardStatusViewController: KeyguardStatusViewController + @Mock private lateinit var KeyguardClockSwitchController: KeyguardClockSwitchController private lateinit var adapter: SplitShadeTransitionAdapter @Before fun setUp() { MockitoAnnotations.initMocks(this) - adapter = SplitShadeTransitionAdapter(keyguardStatusViewController) + adapter = SplitShadeTransitionAdapter(KeyguardClockSwitchController) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserPinMigrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserPinMigrationTest.kt deleted file mode 100644 index 44da5f42aafd..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/ChooserPinMigrationTest.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui - -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.content.res.Resources -import android.testing.AndroidTestingRunner -import androidx.test.filters.SmallTest -import com.android.systemui.broadcast.BroadcastSender -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.kotlinArgumentCaptor -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import java.io.File -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@SmallTest -class ChooserPinMigrationTest : SysuiTestCase() { - - private val fakeFeatureFlags = FakeFeatureFlags() - private val fakePreferences = - mutableMapOf( - "TestPinnedPackage/TestPinnedClass" to true, - "TestUnpinnedPackage/TestUnpinnedClass" to false, - ) - private val intent = kotlinArgumentCaptor<Intent>() - private val permission = kotlinArgumentCaptor<String>() - - private lateinit var chooserPinMigration: ChooserPinMigration - - @Mock private lateinit var mockContext: Context - @Mock private lateinit var mockResources: Resources - @Mock - private lateinit var mockLegacyPinPrefsFileSupplier: - ChooserPinMigration.Companion.LegacyPinPrefsFileSupplier - @Mock private lateinit var mockFile: File - @Mock private lateinit var mockSharedPreferences: SharedPreferences - @Mock private lateinit var mockSharedPreferencesEditor: SharedPreferences.Editor - @Mock private lateinit var mockBroadcastSender: BroadcastSender - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - - whenever(mockContext.resources).thenReturn(mockResources) - whenever(mockContext.getSharedPreferences(any<File>(), anyInt())) - .thenReturn(mockSharedPreferences) - whenever(mockResources.getString(anyInt())).thenReturn("TestPackage/TestClass") - whenever(mockSharedPreferences.all).thenReturn(fakePreferences) - whenever(mockSharedPreferences.edit()).thenReturn(mockSharedPreferencesEditor) - whenever(mockSharedPreferencesEditor.commit()).thenReturn(true) - whenever(mockLegacyPinPrefsFileSupplier.get()).thenReturn(mockFile) - whenever(mockFile.exists()).thenReturn(true) - whenever(mockFile.delete()).thenReturn(true) - fakeFeatureFlags.set(Flags.CHOOSER_MIGRATION_ENABLED, true) - } - - @Test - fun start_performsMigration() { - // Arrange - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verify(mockBroadcastSender).sendBroadcast(intent.capture(), permission.capture()) - assertThat(intent.value.action).isEqualTo("android.intent.action.CHOOSER_PIN_MIGRATION") - assertThat(intent.value.`package`).isEqualTo("TestPackage") - assertThat(intent.value.extras?.keySet()).hasSize(2) - assertThat(intent.value.hasExtra("TestPinnedPackage/TestPinnedClass")).isTrue() - assertThat(intent.value.getBooleanExtra("TestPinnedPackage/TestPinnedClass", false)) - .isTrue() - assertThat(intent.value.hasExtra("TestUnpinnedPackage/TestUnpinnedClass")).isTrue() - assertThat(intent.value.getBooleanExtra("TestUnpinnedPackage/TestUnpinnedClass", true)) - .isFalse() - assertThat(permission.value).isEqualTo("android.permission.RECEIVE_CHOOSER_PIN_MIGRATION") - - // Assert - verify(mockSharedPreferencesEditor).clear() - verify(mockSharedPreferencesEditor).commit() - - // Assert - verify(mockFile).delete() - } - - @Test - fun start_doesNotDeleteLegacyPreferencesFile_whenClearingItFails() { - // Arrange - whenever(mockSharedPreferencesEditor.commit()).thenReturn(false) - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verify(mockBroadcastSender).sendBroadcast(intent.capture(), permission.capture()) - assertThat(intent.value.action).isEqualTo("android.intent.action.CHOOSER_PIN_MIGRATION") - assertThat(intent.value.`package`).isEqualTo("TestPackage") - assertThat(intent.value.extras?.keySet()).hasSize(2) - assertThat(intent.value.hasExtra("TestPinnedPackage/TestPinnedClass")).isTrue() - assertThat(intent.value.getBooleanExtra("TestPinnedPackage/TestPinnedClass", false)) - .isTrue() - assertThat(intent.value.hasExtra("TestUnpinnedPackage/TestUnpinnedClass")).isTrue() - assertThat(intent.value.getBooleanExtra("TestUnpinnedPackage/TestUnpinnedClass", true)) - .isFalse() - assertThat(permission.value).isEqualTo("android.permission.RECEIVE_CHOOSER_PIN_MIGRATION") - - // Assert - verify(mockSharedPreferencesEditor).clear() - verify(mockSharedPreferencesEditor).commit() - - // Assert - verify(mockFile, never()).delete() - } - - @Test - fun start_OnlyDeletesLegacyPreferencesFile_whenEmpty() { - // Arrange - whenever(mockSharedPreferences.all).thenReturn(emptyMap()) - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verifyZeroInteractions(mockBroadcastSender) - - // Assert - verifyZeroInteractions(mockSharedPreferencesEditor) - - // Assert - verify(mockFile).delete() - } - - @Test - fun start_DoesNotDoMigration_whenFlagIsDisabled() { - // Arrange - fakeFeatureFlags.set(Flags.CHOOSER_MIGRATION_ENABLED, false) - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verifyZeroInteractions(mockBroadcastSender) - - // Assert - verifyZeroInteractions(mockSharedPreferencesEditor) - - // Assert - verify(mockFile, never()).delete() - } - - @Test - fun start_DoesNotDoMigration_whenLegacyPreferenceFileNotPresent() { - // Arrange - whenever(mockFile.exists()).thenReturn(false) - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verifyZeroInteractions(mockBroadcastSender) - - // Assert - verifyZeroInteractions(mockSharedPreferencesEditor) - - // Assert - verify(mockFile, never()).delete() - } - - @Test - fun start_DoesNotDoMigration_whenConfiguredChooserComponentIsInvalid() { - // Arrange - whenever(mockResources.getString(anyInt())).thenReturn("InvalidComponent") - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verifyZeroInteractions(mockBroadcastSender) - - // Assert - verifyZeroInteractions(mockSharedPreferencesEditor) - - // Assert - verify(mockFile, never()).delete() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt new file mode 100644 index 000000000000..01d3a3931052 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui + +import android.graphics.Point +import android.hardware.display.DisplayManagerGlobal +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.Display +import android.view.DisplayAdjustments +import android.view.DisplayInfo +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.biometrics.AuthController +import com.android.systemui.decor.FaceScanningProviderFactory +import com.android.systemui.dump.logcatLogBuffer +import com.android.systemui.log.ScreenDecorationsLogger +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +@SmallTest +class FaceScanningProviderFactoryTest : SysuiTestCase() { + + private lateinit var underTest: FaceScanningProviderFactory + + @Mock private lateinit var authController: AuthController + + @Mock private lateinit var statusBarStateController: StatusBarStateController + + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + + @Mock private lateinit var display: Display + + private val displayId = 2 + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + val displayInfo = DisplayInfo() + val dmGlobal = mock(DisplayManagerGlobal::class.java) + val display = + Display( + dmGlobal, + displayId, + displayInfo, + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS + ) + whenever(dmGlobal.getDisplayInfo(eq(displayId))).thenReturn(displayInfo) + val displayContext = context.createDisplayContext(display) as SysuiTestableContext + displayContext.orCreateTestableResources.addOverride( + R.array.config_displayUniqueIdArray, + arrayOf(displayId) + ) + displayContext.orCreateTestableResources.addOverride( + R.bool.config_fillMainBuiltInDisplayCutout, + true + ) + underTest = + FaceScanningProviderFactory( + authController, + displayContext, + statusBarStateController, + keyguardUpdateMonitor, + mock(Executor::class.java), + ScreenDecorationsLogger(logcatLogBuffer("FaceScanningProviderFactoryTest")) + ) + + whenever(authController.faceSensorLocation).thenReturn(Point(10, 10)) + } + + @Test + fun shouldNotShowFaceScanningAnimationIfFaceIsNotEnrolled() { + whenever(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(false) + whenever(authController.isShowing).thenReturn(true) + + assertThat(underTest.shouldShowFaceScanningAnim()).isFalse() + } + + @Test + fun shouldShowFaceScanningAnimationIfBiometricPromptIsShowing() { + whenever(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(true) + whenever(authController.isShowing).thenReturn(true) + + assertThat(underTest.shouldShowFaceScanningAnim()).isTrue() + } + + @Test + fun shouldShowFaceScanningAnimationIfKeyguardFaceDetectionIsShowing() { + whenever(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(true) + whenever(keyguardUpdateMonitor.isFaceDetectionRunning).thenReturn(true) + + assertThat(underTest.shouldShowFaceScanningAnim()).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 6e002f5a9a9a..2489e043c7db 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -59,11 +59,10 @@ import com.android.systemui.statusbar.phone.FakeKeyguardStateController import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.KotlinArgumentCaptor +import com.android.systemui.util.mockito.captureMany import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.SystemClock import com.google.common.truth.Truth.assertThat -import java.io.PrintWriter -import java.io.StringWriter import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -81,6 +80,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.isNull import org.mockito.Mockito.mock @@ -88,6 +88,8 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations +import java.io.PrintWriter +import java.io.StringWriter @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -120,6 +122,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { private lateinit var authStatus: FlowValue<AuthenticationStatus?> private lateinit var detectStatus: FlowValue<DetectionStatus?> private lateinit var authRunning: FlowValue<Boolean?> + private lateinit var bypassEnabled: FlowValue<Boolean?> private lateinit var lockedOut: FlowValue<Boolean?> private lateinit var canFaceAuthRun: FlowValue<Boolean?> private lateinit var authenticated: FlowValue<Boolean?> @@ -726,6 +729,23 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test + fun isBypassEnabledReflectsBypassControllerState() = + testScope.runTest { + initCollectors() + runCurrent() + val listeners = captureMany { + verify(bypassController, atLeastOnce()) + .registerOnBypassStateChangedListener(capture()) + } + + listeners.forEach { it.onBypassStateChanged(true) } + assertThat(bypassEnabled()).isTrue() + + listeners.forEach { it.onBypassStateChanged(false) } + assertThat(bypassEnabled()).isFalse() + } + + @Test fun detectDoesNotRunWhenNonStrongBiometricIsAllowed() = testScope.runTest { testGatingCheckForDetect { @@ -844,6 +864,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { lockedOut = collectLastValue(underTest.isLockedOut) canFaceAuthRun = collectLastValue(underTest.canRunFaceAuth) authenticated = collectLastValue(underTest.isAuthenticated) + bypassEnabled = collectLastValue(underTest.isBypassEnabled) fakeUserRepository.setSelectedUserInfo(primaryUser) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index aace5661862b..1e465c791795 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color @@ -42,6 +43,7 @@ import android.os.Bundle import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.view.animation.Interpolator @@ -101,7 +103,6 @@ import dagger.Lazy import junit.framework.Assert.assertTrue import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -2200,8 +2201,7 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test - @Ignore("b/276920368") - fun bindRecommendation_carouselNotFitThreeRecs() { + fun bindRecommendation_carouselNotFitThreeRecs_OrientationPortrait() { useRealConstraintSets() setupUpdatedRecommendationViewHolder() val albumArt = getColorIcon(Color.RED) @@ -2229,16 +2229,84 @@ public class MediaControlPanelTest : SysuiTestCase() { // set the screen width less than the width of media controls. player.context.resources.configuration.screenWidthDp = 350 + player.context.resources.configuration.orientation = Configuration.ORIENTATION_PORTRAIT player.attachRecommendation(recommendationViewHolder) player.bindRecommendation(data) - assertThat(player.numberOfFittedRecommendations).isEqualTo(2) - assertThat(expandedSet.getVisibility(coverContainer1.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(collapsedSet.getVisibility(coverContainer1.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(expandedSet.getVisibility(coverContainer2.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(collapsedSet.getVisibility(coverContainer2.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(expandedSet.getVisibility(coverContainer3.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(coverContainer3.id)).isEqualTo(ConstraintSet.GONE) + val res = player.context.resources + val displayAvailableWidth = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350f, res.displayMetrics).toInt() + val recCoverWidth: Int = + (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) + + res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2) + val numOfRecs = displayAvailableWidth / recCoverWidth + + assertThat(player.numberOfFittedRecommendations).isEqualTo(numOfRecs) + recommendationViewHolder.mediaCoverContainers.forEachIndexed { index, container -> + if (index < numOfRecs) { + assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.VISIBLE) + assertThat(collapsedSet.getVisibility(container.id)) + .isEqualTo(ConstraintSet.VISIBLE) + } else { + assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) + assertThat(collapsedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) + } + } + } + + @Test + fun bindRecommendation_carouselNotFitThreeRecs_OrientationLandscape() { + useRealConstraintSets() + setupUpdatedRecommendationViewHolder() + val albumArt = getColorIcon(Color.RED) + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "title1") + .setSubtitle("subtitle1") + .setIcon(albumArt) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("subtitle1") + .setIcon(albumArt) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "title3") + .setSubtitle("subtitle1") + .setIcon(albumArt) + .setExtras(Bundle.EMPTY) + .build() + ) + ) + + // set the screen width less than the width of media controls. + // We should have dp width less than 378 to test. In landscape we should have 2x. + player.context.resources.configuration.screenWidthDp = 700 + player.context.resources.configuration.orientation = Configuration.ORIENTATION_LANDSCAPE + player.attachRecommendation(recommendationViewHolder) + player.bindRecommendation(data) + + val res = player.context.resources + val displayAvailableWidth = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350f, res.displayMetrics).toInt() + val recCoverWidth: Int = + (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) + + res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2) + val numOfRecs = displayAvailableWidth / recCoverWidth + + assertThat(player.numberOfFittedRecommendations).isEqualTo(numOfRecs) + recommendationViewHolder.mediaCoverContainers.forEachIndexed { index, container -> + if (index < numOfRecs) { + assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.VISIBLE) + assertThat(collapsedSet.getVisibility(container.id)) + .isEqualTo(ConstraintSet.VISIBLE) + } else { + assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) + assertThat(collapsedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) + } + } } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index 34d2b14d46a9..aa92177e863e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -51,8 +51,10 @@ import com.android.internal.util.CollectionUtils; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; -import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.nano.SystemUIProtoDump; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.qs.QSFactory; @@ -62,7 +64,6 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.external.CustomTileStatePersister; import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; -import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.settings.UserFileManager; @@ -110,8 +111,6 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private Provider<AutoTileManager> mAutoTiles; @Mock - private DumpManager mDumpManager; - @Mock private CentralSurfaces mCentralSurfaces; @Mock private QSLogger mQSLogger; @@ -125,10 +124,6 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private CustomTileStatePersister mCustomTileStatePersister; @Mock - private TileServiceRequestController.Builder mTileServiceRequestControllerBuilder; - @Mock - private TileServiceRequestController mTileServiceRequestController; - @Mock private TileLifecycleManager.Factory mTileLifecycleManagerFactory; @Mock private TileLifecycleManager mTileLifecycleManager; @@ -137,6 +132,8 @@ public class QSTileHostTest extends SysuiTestCase { private SparseArray<SharedPreferences> mSharedPreferencesByUser; + private FakeFeatureFlags mFeatureFlags; + private FakeExecutor mMainExecutor; private QSTileHost mQSTileHost; @@ -144,12 +141,13 @@ public class QSTileHostTest extends SysuiTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); + mFeatureFlags = new FakeFeatureFlags(); + + mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false); + mMainExecutor = new FakeExecutor(new FakeSystemClock()); mSharedPreferencesByUser = new SparseArray<>(); - - when(mTileServiceRequestControllerBuilder.create(any())) - .thenReturn(mTileServiceRequestController); when(mTileLifecycleManagerFactory.create(any(Intent.class), any(UserHandle.class))) .thenReturn(mTileLifecycleManager); when(mUserFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())) @@ -165,10 +163,9 @@ public class QSTileHostTest extends SysuiTestCase { mSecureSettings = new FakeSettings(); saveSetting(""); mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor, - mPluginManager, mTunerService, mAutoTiles, mDumpManager, mCentralSurfaces, + mPluginManager, mTunerService, mAutoTiles, mCentralSurfaces, mQSLogger, mUiEventLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister, - mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory, - mUserFileManager); + mTileLifecycleManagerFactory, mUserFileManager, mFeatureFlags); mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) { @Override @@ -686,18 +683,16 @@ public class QSTileHostTest extends SysuiTestCase { TestQSTileHost(Context context, QSFactory defaultFactory, Executor mainExecutor, PluginManager pluginManager, TunerService tunerService, - Provider<AutoTileManager> autoTiles, DumpManager dumpManager, + Provider<AutoTileManager> autoTiles, CentralSurfaces centralSurfaces, QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, - TileServiceRequestController.Builder tileServiceRequestControllerBuilder, TileLifecycleManager.Factory tileLifecycleManagerFactory, - UserFileManager userFileManager) { + UserFileManager userFileManager, FeatureFlags featureFlags) { super(context, defaultFactory, mainExecutor, pluginManager, - tunerService, autoTiles, dumpManager, Optional.of(centralSurfaces), qsLogger, + tunerService, autoTiles, Optional.of(centralSurfaces), qsLogger, uiEventLogger, userTracker, secureSettings, customTileStatePersister, - tileServiceRequestControllerBuilder, tileLifecycleManagerFactory, - userFileManager); + tileLifecycleManagerFactory, userFileManager, featureFlags); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt index c03849b35f54..50a8d2630d79 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -170,6 +170,21 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { } @Test + fun addTileAtPosition_tooLarge_addedAtEnd() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,custom(b/c)" + storeTilesForUser(specs, 0) + + underTest.addTile(userId = 0, TileSpec.create("d"), position = 100) + + val expected = "a,custom(b/c),d" + assertThat(loadTilesForUser(0)).isEqualTo(expected) + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + } + + @Test fun addTileForOtherUser_addedInThatUser() = testScope.runTest { val tilesUser0 by collectLastValue(underTest.tilesSpecs(0)) @@ -187,27 +202,27 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { } @Test - fun removeTile() = + fun removeTiles() = testScope.runTest { val tiles by collectLastValue(underTest.tilesSpecs(0)) storeTilesForUser("a,b", 0) - underTest.removeTile(userId = 0, TileSpec.create("a")) + underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"))) assertThat(loadTilesForUser(0)).isEqualTo("b") assertThat(tiles).isEqualTo("b".toTileSpecs()) } @Test - fun removeTileNotThere_noop() = + fun removeTilesNotThere_noop() = testScope.runTest { val tiles by collectLastValue(underTest.tilesSpecs(0)) val specs = "a,b" storeTilesForUser(specs, 0) - underTest.removeTile(userId = 0, TileSpec.create("c")) + underTest.removeTiles(userId = 0, listOf(TileSpec.create("c"))) assertThat(loadTilesForUser(0)).isEqualTo(specs) assertThat(tiles).isEqualTo(specs.toTileSpecs()) @@ -221,7 +236,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { val specs = "a,b" storeTilesForUser(specs, 0) - underTest.removeTile(userId = 0, TileSpec.Invalid) + underTest.removeTiles(userId = 0, listOf(TileSpec.Invalid)) assertThat(loadTilesForUser(0)).isEqualTo(specs) assertThat(tiles).isEqualTo(specs.toTileSpecs()) @@ -237,7 +252,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { storeTilesForUser(specs, 0) storeTilesForUser(specs, 1) - underTest.removeTile(userId = 1, TileSpec.create("a")) + underTest.removeTiles(userId = 1, listOf(TileSpec.create("a"))) assertThat(loadTilesForUser(0)).isEqualTo(specs) assertThat(user0Tiles).isEqualTo(specs.toTileSpecs()) @@ -246,6 +261,19 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { } @Test + fun removeMultipleTiles() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a,b,c,d", 0) + + underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"), TileSpec.create("c"))) + + assertThat(loadTilesForUser(0)).isEqualTo("b,d") + assertThat(tiles).isEqualTo("b,d".toTileSpecs()) + } + + @Test fun changeTiles() = testScope.runTest { val tiles by collectLastValue(underTest.tilesSpecs(0)) @@ -310,8 +338,8 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { storeTilesForUser(specs, 0) coroutineScope { - underTest.removeTile(userId = 0, TileSpec.create("c")) - underTest.removeTile(userId = 0, TileSpec.create("a")) + underTest.removeTiles(userId = 0, listOf(TileSpec.create("c"))) + underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"))) } assertThat(loadTilesForUser(0)).isEqualTo("b") diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt new file mode 100644 index 000000000000..7ecb4dc9376a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -0,0 +1,674 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.domain.interactor + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.os.UserHandle +import android.service.quicksettings.Tile +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.plugins.qs.QSTile.BooleanState +import com.android.systemui.qs.FakeQSFactory +import com.android.systemui.qs.external.CustomTile +import com.android.systemui.qs.external.CustomTileStatePersister +import com.android.systemui.qs.external.TileLifecycleManager +import com.android.systemui.qs.external.TileServiceKey +import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository +import com.android.systemui.qs.pipeline.data.repository.FakeCustomTileAddedRepository +import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.pipeline.domain.model.TileModel +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.qs.toProto +import com.android.systemui.settings.UserTracker +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import com.google.protobuf.nano.MessageNano +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) +class CurrentTilesInteractorImplTest : SysuiTestCase() { + + private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository() + private val userRepository = FakeUserRepository() + private val tileFactory = FakeQSFactory(::tileCreator) + private val customTileAddedRepository: CustomTileAddedRepository = + FakeCustomTileAddedRepository() + private val featureFlags = FakeFeatureFlags() + private val tileLifecycleManagerFactory = TLMFactory() + + @Mock private lateinit var customTileStatePersister: CustomTileStatePersister + + @Mock private lateinit var userTracker: UserTracker + + @Mock private lateinit var logger: QSPipelineLogger + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val unavailableTiles = mutableSetOf("e") + + private lateinit var underTest: CurrentTilesInteractorImpl + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true) + + userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1)) + setUserTracker(0) + + underTest = + CurrentTilesInteractorImpl( + tileSpecRepository = tileSpecRepository, + userRepository = userRepository, + customTileStatePersister = customTileStatePersister, + tileFactory = tileFactory, + customTileAddedRepository = customTileAddedRepository, + tileLifecycleManagerFactory = tileLifecycleManagerFactory, + userTracker = userTracker, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, + scope = testScope.backgroundScope, + logger = logger, + featureFlags = featureFlags, + ) + } + + @Test + fun initialState() = + testScope.runTest(USER_INFO_0) { + assertThat(underTest.currentTiles.value).isEmpty() + assertThat(underTest.currentQSTiles).isEmpty() + assertThat(underTest.currentTilesSpecs).isEmpty() + assertThat(underTest.userId.value).isEqualTo(0) + assertThat(underTest.userContext.value.userId).isEqualTo(0) + } + + @Test + fun correctTiles() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = + listOf( + TileSpec.create("a"), + TileSpec.create("e"), + CUSTOM_TILE_SPEC, + TileSpec.create("d"), + TileSpec.create("non_existent") + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + // check each tile + + // Tile a + val tile0 = tiles!![0] + assertThat(tile0.spec).isEqualTo(specs[0]) + assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec) + assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java) + assertThat(tile0.tile.isAvailable).isTrue() + + // Tile e is not available and is not in the list + + // Custom Tile + val tile1 = tiles!![1] + assertThat(tile1.spec).isEqualTo(specs[2]) + assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec) + assertThat(tile1.tile).isInstanceOf(CustomTile::class.java) + assertThat(tile1.tile.isAvailable).isTrue() + + // Tile d + val tile2 = tiles!![2] + assertThat(tile2.spec).isEqualTo(specs[3]) + assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec) + assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java) + assertThat(tile2.tile.isAvailable).isTrue() + + // Tile non-existent shouldn't be created. Therefore, only 3 tiles total + assertThat(tiles?.size).isEqualTo(3) + } + + @Test + fun logTileCreated() = + testScope.runTest(USER_INFO_0) { + val specs = + listOf( + TileSpec.create("a"), + CUSTOM_TILE_SPEC, + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + specs.forEach { verify(logger).logTileCreated(it) } + } + + @Test + fun logTileNotFoundInFactory() = + testScope.runTest(USER_INFO_0) { + val specs = + listOf( + TileSpec.create("non_existing"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + verify(logger, never()).logTileCreated(any()) + verify(logger).logTileNotFoundInFactory(specs[0]) + } + + @Test + fun tileNotAvailableDestroyed_logged() = + testScope.runTest(USER_INFO_0) { + val specs = + listOf( + TileSpec.create("e"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + verify(logger, never()).logTileCreated(any()) + verify(logger) + .logTileDestroyed( + specs[0], + QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE + ) + } + + @Test + fun someTilesNotValid_repositorySetToDefinitiveList() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = + listOf( + TileSpec.create("a"), + TileSpec.create("e"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) + } + + @Test + fun deduplicatedTiles() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = listOf(TileSpec.create("a"), TileSpec.create("a")) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(specs[0]) + } + + @Test + fun tilesChange_platformTileNotRecreated() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = + listOf( + TileSpec.create("a"), + ) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileA = tiles!![0].tile + + tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b")) + + assertThat(tiles?.size).isEqualTo(2) + assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA) + } + + @Test + fun tileRemovedIsDestroyed() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = listOf(TileSpec.create("a"), TileSpec.create("c")) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileC = tiles!![1].tile + + tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c"))) + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a")) + + assertThat((originalTileC as FakeQSTile).destroyed).isTrue() + verify(logger) + .logTileDestroyed( + TileSpec.create("c"), + QSPipelineLogger.TileDestroyedReason.TILE_REMOVED + ) + } + + @Test + fun tileBecomesNotAvailable_destroyed() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = listOf(TileSpec.create("a")) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileA = tiles!![0].tile + + // Tile becomes unavailable + (originalTileA as FakeQSTile).available = false + unavailableTiles.add("a") + // and there is some change in the specs + tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b")) + runCurrent() + + assertThat(originalTileA.destroyed).isTrue() + verify(logger) + .logTileDestroyed( + TileSpec.create("a"), + QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE + ) + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b")) + assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA) + + assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec)) + } + + @Test + fun userChange_tilesChange() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs0 = listOf(TileSpec.create("a")) + val specs1 = listOf(TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs0) + tileSpecRepository.setTiles(USER_INFO_1.id, specs1) + + switchUser(USER_INFO_1) + + assertThat(tiles!![0].spec).isEqualTo(specs1[0]) + assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec) + } + + @Test + fun tileNotPresentInSecondaryUser_destroyedInUserChange() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs0 = listOf(TileSpec.create("a")) + val specs1 = listOf(TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs0) + tileSpecRepository.setTiles(USER_INFO_1.id, specs1) + + val originalTileA = tiles!![0].tile + + switchUser(USER_INFO_1) + runCurrent() + + assertThat((originalTileA as FakeQSTile).destroyed).isTrue() + verify(logger) + .logTileDestroyed( + specs0[0], + QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER + ) + } + + @Test + fun userChange_customTileDestroyed_lifecycleNotTerminated() { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = listOf(CUSTOM_TILE_SPEC) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + tileSpecRepository.setTiles(USER_INFO_1.id, specs) + + val originalCustomTile = tiles!![0].tile + + switchUser(USER_INFO_1) + runCurrent() + + verify(originalCustomTile).destroy() + assertThat(tileLifecycleManagerFactory.created).isEmpty() + } + } + + @Test + fun userChange_sameTileUserChanged() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = listOf(TileSpec.create("a")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + tileSpecRepository.setTiles(USER_INFO_1.id, specs) + + val originalTileA = tiles!![0].tile as FakeQSTile + assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id) + + switchUser(USER_INFO_1) + runCurrent() + + assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA) + assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id) + verify(logger).logTileUserChanged(specs[0], USER_INFO_1.id) + } + + @Test + fun addTile() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val spec = TileSpec.create("a") + val currentSpecs = + listOf( + TileSpec.create("b"), + TileSpec.create("c"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + + underTest.addTile(spec, position = 1) + + val expectedSpecs = + listOf( + TileSpec.create("b"), + spec, + TileSpec.create("c"), + ) + assertThat(tiles).isEqualTo(expectedSpecs) + } + + @Test + fun addTile_currentUser() = + testScope.runTest(USER_INFO_1) { + val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id)) + val spec = TileSpec.create("a") + val currentSpecs = + listOf( + TileSpec.create("b"), + TileSpec.create("c"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs) + + switchUser(USER_INFO_1) + underTest.addTile(spec, position = 1) + + assertThat(tiles0).isEqualTo(currentSpecs) + + val expectedSpecs = + listOf( + TileSpec.create("b"), + spec, + TileSpec.create("c"), + ) + assertThat(tiles1).isEqualTo(expectedSpecs) + } + + @Test + fun removeTile_platform() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = listOf(TileSpec.create("a"), TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + underTest.removeTiles(specs.subList(0, 1)) + + assertThat(tiles).isEqualTo(specs.subList(1, 2)) + } + + @Test + fun removeTile_customTile_lifecycleEnded() { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isTrue() + + underTest.removeTiles(listOf(CUSTOM_TILE_SPEC)) + + assertThat(tiles).isEqualTo(specs.subList(0, 1)) + + val tileLifecycleManager = + tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT] + assertThat(tileLifecycleManager).isNotNull() + + with(inOrder(tileLifecycleManager!!)) { + verify(tileLifecycleManager).onStopListening() + verify(tileLifecycleManager).onTileRemoved() + verify(tileLifecycleManager).flushMessagesAndUnbind() + } + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isFalse() + verify(customTileStatePersister) + .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)) + } + } + + @Test + fun removeTiles_currentUser() = + testScope.runTest { + val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id)) + val currentSpecs = + listOf( + TileSpec.create("a"), + TileSpec.create("b"), + TileSpec.create("c"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs) + + switchUser(USER_INFO_1) + runCurrent() + + underTest.removeTiles(currentSpecs.subList(0, 2)) + + assertThat(tiles0).isEqualTo(currentSpecs) + assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3)) + } + + @Test + fun setTiles() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + runCurrent() + + val newSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a")) + underTest.setTiles(newSpecs) + runCurrent() + + assertThat(tiles).isEqualTo(newSpecs) + } + + @Test + fun setTiles_customTiles_lifecycleEndedIfGone() = + testScope.runTest(USER_INFO_0) { + val otherCustomTileSpec = TileSpec.create("custom(b/c)") + + val currentSpecs = listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + runCurrent() + + val newSpecs = + listOf( + otherCustomTileSpec, + TileSpec.create("a"), + ) + + underTest.setTiles(newSpecs) + runCurrent() + + val tileLifecycleManager = + tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]!! + + with(inOrder(tileLifecycleManager)) { + verify(tileLifecycleManager).onStopListening() + verify(tileLifecycleManager).onTileRemoved() + verify(tileLifecycleManager).flushMessagesAndUnbind() + } + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isFalse() + verify(customTileStatePersister) + .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)) + } + + @Test + fun protoDump() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + val stateA = tiles!![0].tile.state + stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA") + val stateCustom = QSTile.BooleanState() + stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB") + stateCustom.spec = CUSTOM_TILE_SPEC.spec + whenever(tiles!![1].tile.state).thenReturn(stateCustom) + + val proto = SystemUIProtoDump() + underTest.dumpProto(proto, emptyArray()) + + assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue() + assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto())) + .isTrue() + } + + private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) { + this.state = state + this.label = label + this.secondaryLabel = secondaryLabel + if (this is BooleanState) { + value = state == Tile.STATE_ACTIVE + } + } + + private fun tileCreator(spec: String): QSTile? { + val currentUser = userTracker.userId + return when (spec) { + CUSTOM_TILE_SPEC.spec -> + mock<CustomTile> { + var tileSpecReference: String? = null + whenever(user).thenReturn(currentUser) + whenever(component).thenReturn(CUSTOM_TILE_SPEC.componentName) + whenever(isAvailable).thenReturn(true) + whenever(setTileSpec(anyString())).thenAnswer { + tileSpecReference = it.arguments[0] as? String + Unit + } + whenever(tileSpec).thenAnswer { tileSpecReference } + // Also, add it to the set of added tiles (as this happens as part of the tile + // creation). + customTileAddedRepository.setTileAdded( + CUSTOM_TILE_SPEC.componentName, + currentUser, + true + ) + } + in VALID_TILES -> FakeQSTile(currentUser, available = spec !in unavailableTiles) + else -> null + } + } + + private fun TestScope.runTest(user: UserInfo, body: suspend TestScope.() -> Unit) { + return runTest { + switchUser(user) + body() + } + } + + private suspend fun switchUser(user: UserInfo) { + setUserTracker(user.id) + userRepository.setSelectedUserInfo(user) + } + + private fun setUserTracker(user: Int) { + val mockContext = mockUserContext(user) + whenever(userTracker.userContext).thenReturn(mockContext) + whenever(userTracker.userId).thenReturn(user) + } + + private class TLMFactory : TileLifecycleManager.Factory { + + val created = mutableMapOf<Pair<Int, ComponentName>, TileLifecycleManager>() + + override fun create(intent: Intent, userHandle: UserHandle): TileLifecycleManager { + val componentName = intent.component!! + val user = userHandle.identifier + val manager: TileLifecycleManager = mock() + created[user to componentName] = manager + return manager + } + } + + private fun mockUserContext(user: Int): Context { + return mock { + whenever(this.userId).thenReturn(user) + whenever(this.user).thenReturn(UserHandle.of(user)) + } + } + + companion object { + private val USER_INFO_0 = UserInfo().apply { id = 0 } + private val USER_INFO_1 = UserInfo().apply { id = 1 } + + private val VALID_TILES = setOf("a", "b", "c", "d", "e") + private val TEST_COMPONENT = ComponentName("pkg", "cls") + private val CUSTOM_TILE_SPEC = TileSpec.Companion.create(TEST_COMPONENT) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt new file mode 100644 index 000000000000..e50969641a71 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.domain.interactor + +import android.content.Context +import android.view.View +import com.android.internal.logging.InstanceId +import com.android.systemui.plugins.qs.QSIconView +import com.android.systemui.plugins.qs.QSTile + +class FakeQSTile( + var user: Int, + var available: Boolean = true, +) : QSTile { + private var tileSpec: String? = null + var destroyed = false + private val state = QSTile.State() + + override fun getTileSpec(): String? { + return tileSpec + } + + override fun isAvailable(): Boolean { + return available + } + + override fun setTileSpec(tileSpec: String?) { + this.tileSpec = tileSpec + state.spec = tileSpec + } + + override fun refreshState() {} + + override fun addCallback(callback: QSTile.Callback?) {} + + override fun removeCallback(callback: QSTile.Callback?) {} + + override fun removeCallbacks() {} + + override fun createTileView(context: Context?): QSIconView? { + return null + } + + override fun click(view: View?) {} + + override fun secondaryClick(view: View?) {} + + override fun longClick(view: View?) {} + + override fun userSwitch(currentUser: Int) { + user = currentUser + } + + override fun getMetricsCategory(): Int { + return 0 + } + + override fun setListening(client: Any?, listening: Boolean) {} + + override fun setDetailListening(show: Boolean) {} + + override fun destroy() { + destroyed = true + } + + override fun getTileLabel(): CharSequence { + return "" + } + + override fun getState(): QSTile.State { + return state + } + + override fun getInstanceId(): InstanceId { + return InstanceId.fakeInstanceId(0) + } + + override fun isListening(): Boolean { + return false + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 99979976a122..5ca37716cbff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -32,6 +32,7 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -66,6 +67,7 @@ import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardClockSwitch; import com.android.keyguard.KeyguardClockSwitchController; +import com.android.keyguard.KeyguardSliceViewController; import com.android.keyguard.KeyguardStatusView; import com.android.keyguard.KeyguardStatusViewController; import com.android.keyguard.KeyguardUpdateMonitor; @@ -74,6 +76,7 @@ import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent; import com.android.keyguard.dagger.KeyguardStatusBarViewComponent; import com.android.keyguard.dagger.KeyguardStatusViewComponent; import com.android.keyguard.dagger.KeyguardUserSwitcherComponent; +import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; @@ -233,7 +236,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory; @Mock protected KeyguardStatusBarViewComponent mKeyguardStatusBarViewComponent; @Mock protected KeyguardClockSwitchController mKeyguardClockSwitchController; - @Mock protected KeyguardStatusViewController mKeyguardStatusViewController; @Mock protected KeyguardStatusBarViewController mKeyguardStatusBarViewController; @Mock protected NotificationStackScrollLayoutController mNotificationStackScrollLayoutController; @@ -293,6 +295,9 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock protected MotionEvent mDownMotionEvent; @Mock protected CoroutineDispatcher mMainDispatcher; + @Mock protected KeyguardSliceViewController mKeyguardSliceViewController; + @Mock protected KeyguardLogger mKeyguardLogger; + @Mock protected KeyguardStatusView mKeyguardStatusView; @Captor protected ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> mEmptySpaceClickListenerCaptor; @@ -309,6 +314,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected List<View.OnAttachStateChangeListener> mOnAttachStateChangeListeners; protected Handler mMainHandler; protected View.OnLayoutChangeListener mLayoutChangeListener; + protected KeyguardStatusViewController mKeyguardStatusViewController; protected final FalsingManagerFake mFalsingManager = new FalsingManagerFake(); protected final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty(); @@ -335,6 +341,18 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext); keyguardStatusView.setId(R.id.keyguard_status_view); + mKeyguardStatusViewController = spy(new KeyguardStatusViewController( + mKeyguardStatusView, + mKeyguardSliceViewController, + mKeyguardClockSwitchController, + mKeyguardStateController, + mUpdateMonitor, + mConfigurationController, + mDozeParameters, + mScreenOffAnimationController, + mKeyguardLogger, + mFeatureFlags, + mInteractionJankMonitor)); when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); when(mHeadsUpCallback.getContext()).thenReturn(mContext); @@ -366,12 +384,15 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { when(mView.findViewById(R.id.keyguard_bottom_area)).thenReturn(mKeyguardBottomArea); when(mKeyguardBottomArea.animate()).thenReturn(mViewPropertyAnimator); when(mView.animate()).thenReturn(mViewPropertyAnimator); + when(mKeyguardStatusView.animate()).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.translationX(anyFloat())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.alpha(anyFloat())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.setDuration(anyLong())).thenReturn(mViewPropertyAnimator); + when(mViewPropertyAnimator.setStartDelay(anyLong())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.setInterpolator(any())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.setListener(any())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.setUpdateListener(any())).thenReturn(mViewPropertyAnimator); + when(mViewPropertyAnimator.withEndAction(any())).thenReturn(mViewPropertyAnimator); when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame); when(mView.findViewById(R.id.keyguard_status_view)) .thenReturn(mock(KeyguardStatusView.class)); @@ -650,9 +671,13 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @After public void tearDown() { - mNotificationPanelViewController.mBottomAreaShadeAlphaAnimator.cancel(); - mNotificationPanelViewController.cancelHeightAnimator(); - mMainHandler.removeCallbacksAndMessages(null); + if (mNotificationPanelViewController != null) { + mNotificationPanelViewController.mBottomAreaShadeAlphaAnimator.cancel(); + mNotificationPanelViewController.cancelHeightAnimator(); + } + if (mMainHandler != null) { + mMainHandler.removeCallbacksAndMessages(null); + } } protected void setBottomPadding(int stackBottom, int lockIconPadding, int indicationPadding, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java index 569f90b64609..4438b98f6fad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -614,9 +614,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void onBiometricHelp_coEx_faceFailure() { createController(); - // GIVEN unlocking with fingerprint is possible - when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(anyInt())) - .thenReturn(true); + // GIVEN unlocking with fingerprint is possible and allowed + fingerprintUnlockIsPossibleAndAllowed(); String message = "A message"; mController.setVisible(true); @@ -641,9 +640,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void onBiometricHelp_coEx_faceUnavailable() { createController(); - // GIVEN unlocking with fingerprint is possible - when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(anyInt())) - .thenReturn(true); + // GIVEN unlocking with fingerprint is possible and allowed + fingerprintUnlockIsPossibleAndAllowed(); String message = "A message"; mController.setVisible(true); @@ -664,6 +662,35 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mContext.getString(R.string.keyguard_suggest_fingerprint)); } + + @Test + public void onBiometricHelp_coEx_faceUnavailable_fpNotAllowed() { + createController(); + + // GIVEN unlocking with fingerprint is possible but not allowed + setupFingerprintUnlockPossible(true); + when(mKeyguardUpdateMonitor.isUnlockingWithFingerprintAllowed()) + .thenReturn(false); + + String message = "A message"; + mController.setVisible(true); + + // WHEN there's a face unavailable message + mController.getKeyguardCallback().onBiometricHelp( + BIOMETRIC_HELP_FACE_NOT_AVAILABLE, + message, + BiometricSourceType.FACE); + + // THEN show sequential messages such as: 'face unlock unavailable' and + // 'try fingerprint instead' + verifyIndicationMessage( + INDICATION_TYPE_BIOMETRIC_MESSAGE, + message); + verifyIndicationMessage( + INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, + mContext.getString(R.string.keyguard_unlock)); + } + @Test public void onBiometricHelp_coEx_fpFailure_faceAlreadyUnlocked() { createController(); @@ -818,8 +845,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void faceErrorTimeout_whenFingerprintEnrolled_doesNotShowMessage() { createController(); - when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( - getCurrentUser())).thenReturn(true); + fingerprintUnlockIsPossibleAndAllowed(); String message = "A message"; mController.setVisible(true); @@ -832,9 +858,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void sendFaceHelpMessages_fingerprintEnrolled() { createController(); - // GIVEN fingerprint enrolled - when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( - getCurrentUser())).thenReturn(true); + // GIVEN unlocking with fingerprint is possible and allowed + fingerprintUnlockIsPossibleAndAllowed(); // WHEN help messages received that are allowed to show final String helpString = "helpString"; @@ -859,9 +884,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void doNotSendMostFaceHelpMessages_fingerprintEnrolled() { createController(); - // GIVEN fingerprint enrolled - when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( - getCurrentUser())).thenReturn(true); + // GIVEN unlocking with fingerprint is possible and allowed + fingerprintUnlockIsPossibleAndAllowed(); // WHEN help messages received that aren't supposed to show final String helpString = "helpString"; @@ -886,9 +910,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void sendAllFaceHelpMessages_fingerprintNotEnrolled() { createController(); - // GIVEN fingerprint NOT enrolled - when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( - getCurrentUser())).thenReturn(false); + // GIVEN fingerprint NOT possible + fingerprintUnlockIsNotPossible(); // WHEN help messages received final Set<CharSequence> helpStrings = new HashSet<>(); @@ -917,9 +940,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void sendTooDarkFaceHelpMessages_onTimeout_noFpEnrolled() { createController(); - // GIVEN fingerprint NOT enrolled - when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( - getCurrentUser())).thenReturn(false); + // GIVEN fingerprint not possible + fingerprintUnlockIsNotPossible(); // WHEN help message received and deferred message is valid final String helpString = "helpMsg"; @@ -948,9 +970,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void sendTooDarkFaceHelpMessages_onTimeout_fingerprintEnrolled() { createController(); - // GIVEN fingerprint enrolled - when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( - getCurrentUser())).thenReturn(true); + // GIVEN unlocking with fingerprint is possible and allowed + fingerprintUnlockIsPossibleAndAllowed(); // WHEN help message received and deferredMessage is valid final String helpString = "helpMsg"; @@ -1500,7 +1521,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void onBiometricError_faceLockedOutFirstTimeAndFpAllowed_showsTheFpFollowupMessage() { createController(); - fingerprintUnlockIsPossible(); + fingerprintUnlockIsPossibleAndAllowed(); onFaceLockoutError("first lockout"); verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, @@ -1559,7 +1580,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void onBiometricError_faceLockedOutAgainAndFpAllowed_showsTheFpFollowupMessage() { createController(); - fingerprintUnlockIsPossible(); + fingerprintUnlockIsPossibleAndAllowed(); onFaceLockoutError("first lockout"); clearInvocations(mRotateTextViewController); @@ -1668,7 +1689,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void onBiometricError_screenIsTurningOn_faceLockedOutFpIsAvailable_showsMessage() { createController(); screenIsTurningOn(); - fingerprintUnlockIsPossible(); + fingerprintUnlockIsPossibleAndAllowed(); onFaceLockoutError("lockout error"); verifyNoMoreInteractions(mRotateTextViewController); @@ -1746,8 +1767,9 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { setupFingerprintUnlockPossible(false); } - private void fingerprintUnlockIsPossible() { + private void fingerprintUnlockIsPossibleAndAllowed() { setupFingerprintUnlockPossible(true); + when(mKeyguardUpdateMonitor.isUnlockingWithFingerprintAllowed()).thenReturn(true); } private void setupFingerprintUnlockPossible(boolean possible) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt index 7b59cc284181..08a9f3139d71 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -22,7 +22,7 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.View import android.widget.FrameLayout -import androidx.core.animation.AnimatorTestRule2 +import androidx.core.animation.AnimatorTestRule import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager @@ -70,7 +70,7 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler private val fakeFeatureFlags = FakeFeatureFlags() - @get:Rule val animatorTestRule = AnimatorTestRule2() + @get:Rule val animatorTestRule = AnimatorTestRule() @Before fun setup() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt index be3b7234a1a2..aff705f1f3bf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.notification import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import androidx.core.animation.AnimatorTestRule2 +import androidx.core.animation.AnimatorTestRule import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager @@ -51,7 +51,7 @@ import org.mockito.Mockito.verifyNoMoreInteractions @TestableLooper.RunWithLooper(setAsMainLooper = true) class NotificationWakeUpCoordinatorTest : SysuiTestCase() { - @get:Rule val animatorTestRule = AnimatorTestRule2() + @get:Rule val animatorTestRule = AnimatorTestRule() private val dumpManager: DumpManager = mock() private val headsUpManager: HeadsUpManager = mock() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt new file mode 100644 index 000000000000..14e5f9e63ebe --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.shelf.domain.interactor + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class NotificationShelfInteractorTest : SysuiTestCase() { + + private val keyguardRepository = FakeKeyguardRepository() + private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository() + private val underTest = + NotificationShelfInteractor(keyguardRepository, deviceEntryFaceAuthRepository) + + @Test + fun shelfIsNotStatic_whenKeyguardNotShowing() = runTest { + val shelfStatic by collectLastValue(underTest.isShelfStatic) + + keyguardRepository.setKeyguardShowing(false) + + assertThat(shelfStatic).isFalse() + } + + @Test + fun shelfIsNotStatic_whenKeyguardShowingAndNotBypass() = runTest { + val shelfStatic by collectLastValue(underTest.isShelfStatic) + + keyguardRepository.setKeyguardShowing(true) + deviceEntryFaceAuthRepository.isBypassEnabled.value = false + + assertThat(shelfStatic).isFalse() + } + + @Test + fun shelfIsStatic_whenBypass() = runTest { + val shelfStatic by collectLastValue(underTest.isShelfStatic) + + keyguardRepository.setKeyguardShowing(true) + deviceEntryFaceAuthRepository.isBypassEnabled.value = true + + assertThat(shelfStatic).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt new file mode 100644 index 000000000000..6c5fb8bcff22 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.shelf.ui.viewmodel + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class NotificationShelfViewModelTest : SysuiTestCase() { + + private val keyguardRepository = FakeKeyguardRepository() + private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository() + private val interactor = + NotificationShelfInteractor(keyguardRepository, deviceEntryFaceAuthRepository) + private val underTest = NotificationShelfViewModel(interactor) + + @Test + fun canModifyColorOfNotifications_whenKeyguardNotShowing() = runTest { + val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) + + keyguardRepository.setKeyguardShowing(false) + + assertThat(canModifyNotifColor).isTrue() + } + + @Test + fun canModifyColorOfNotifications_whenKeyguardShowingAndNotBypass() = runTest { + val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) + + keyguardRepository.setKeyguardShowing(true) + deviceEntryFaceAuthRepository.isBypassEnabled.value = false + + assertThat(canModifyNotifColor).isTrue() + } + + @Test + fun cannotModifyColorOfNotifications_whenBypass() = runTest { + val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications) + + keyguardRepository.setKeyguardShowing(true) + deviceEntryFaceAuthRepository.isBypassEnabled.value = true + + assertThat(canModifyNotifColor).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index ddc6d484d93f..d30e0246c2dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -29,6 +29,7 @@ import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback +import android.net.wifi.WifiManager.UNKNOWN_SSID import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -49,16 +50,14 @@ import com.android.systemui.util.mockito.nullable import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.concurrent.Executor -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.After +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyInt @@ -80,9 +79,10 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var wifiManager: WifiManager private lateinit var executor: Executor - private lateinit var scope: CoroutineScope private lateinit var connectivityRepository: ConnectivityRepository + private val testScope = TestScope(UnconfinedTestDispatcher()) + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -96,7 +96,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { ) .thenReturn(flowOf(Unit)) executor = FakeExecutor(FakeSystemClock()) - scope = CoroutineScope(IMMEDIATE) connectivityRepository = ConnectivityRepositoryImpl( @@ -105,21 +104,16 @@ class WifiRepositoryImplTest : SysuiTestCase() { context, mock(), mock(), - scope, + testScope.backgroundScope, mock(), ) underTest = createRepo() } - @After - fun tearDown() { - scope.cancel() - } - @Test fun isWifiEnabled_initiallyGetsWifiManagerValue() = - runBlocking(IMMEDIATE) { + testScope.runTest { whenever(wifiManager.isWifiEnabled).thenReturn(true) underTest = createRepo() @@ -129,7 +123,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_networkCapabilitiesChanged_valueUpdated() = - runBlocking(IMMEDIATE) { + testScope.runTest { // We need to call launch on the flows so that they start updating val networkJob = underTest.wifiNetwork.launchIn(this) val enabledJob = underTest.isWifiEnabled.launchIn(this) @@ -152,7 +146,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_networkLost_valueUpdated() = - runBlocking(IMMEDIATE) { + testScope.runTest { // We need to call launch on the flows so that they start updating val networkJob = underTest.wifiNetwork.launchIn(this) val enabledJob = underTest.isWifiEnabled.launchIn(this) @@ -173,7 +167,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_intentsReceived_valueUpdated() = - runBlocking(IMMEDIATE) { + testScope.runTest { val intentFlow = MutableSharedFlow<Unit>() whenever( broadcastDispatcher.broadcastFlow( @@ -203,7 +197,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() = - runBlocking(IMMEDIATE) { + testScope.runTest { val intentFlow = MutableSharedFlow<Unit>() whenever( broadcastDispatcher.broadcastFlow( @@ -242,7 +236,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_initiallyGetsDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) assertThat(underTest.isWifiDefault.value).isFalse() @@ -252,7 +246,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_wifiNetwork_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val wifiInfo = mock<WifiInfo>().apply { whenever(this.ssid).thenReturn(SSID) } @@ -268,7 +262,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { /** Regression test for b/266628069. */ @Test fun isWifiDefault_transportInfoIsNotWifi_andNoWifiTransport_false() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val transportInfo = @@ -294,7 +288,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { /** Regression test for b/266628069. */ @Test fun isWifiDefault_transportInfoIsNotWifi_butHasWifiTransport_true() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val transportInfo = @@ -319,7 +313,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_carrierMergedViaCellular_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val carrierMergedInfo = @@ -341,7 +335,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_carrierMergedViaCellular_withVcnTransport_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val capabilities = @@ -360,7 +354,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_carrierMergedViaWifi_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val carrierMergedInfo = @@ -382,7 +376,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_carrierMergedViaWifi_withVcnTransport_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val capabilities = @@ -401,7 +395,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_cellularAndWifiTransports_usesCellular_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val capabilities = @@ -420,7 +414,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_cellularNotVcnNetwork_isFalse() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val capabilities = @@ -438,7 +432,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_isCarrierMergedViaUnderlyingWifi_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val underlyingNetwork = mock<Network>() @@ -473,7 +467,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_isCarrierMergedViaUnderlyingCellular_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val underlyingCarrierMergedNetwork = mock<Network>() @@ -507,7 +501,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_wifiNetworkLost_isFalse() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) // First, add a network @@ -526,7 +520,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_initiallyGetsDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -537,7 +531,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -561,7 +555,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -581,7 +575,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_isCarrierMergedViaUnderlyingWifi_flowHasCarrierMerged() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -618,7 +612,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_isCarrierMergedViaUnderlyingCellular_flowHasCarrierMerged() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -656,7 +650,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -680,7 +674,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_isCarrierMerged_getsCorrectValues() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -715,7 +709,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_notValidated_networkNotValidated() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -732,7 +726,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_validated_networkValidated() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -749,7 +743,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -770,7 +764,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { /** Regression test for b/266628069. */ @Test fun wifiNetwork_transportInfoIsNotWifi_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -789,7 +783,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -811,7 +805,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -835,7 +829,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -854,7 +848,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_cellularAndWifiTransports_usesCellular() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -877,7 +871,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -910,7 +904,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -943,7 +937,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_newNetworkCapabilities_flowHasNewData() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -986,7 +980,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -1001,7 +995,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_currentNetworkLost_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -1020,7 +1014,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -1043,7 +1037,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -1069,7 +1063,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { /** Regression test for b/244173280. */ @Test fun wifiNetwork_multipleSubscribers_newSubscribersGetCurrentValue() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest1: WifiNetworkModel? = null val job1 = underTest.wifiNetwork.onEach { latest1 = it }.launchIn(this) @@ -1096,8 +1090,151 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test + fun isWifiConnectedWithValidSsid_inactiveNetwork_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.ssid).thenReturn(SSID) + // A non-primary network is inactive + whenever(this.isPrimary).thenReturn(false) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_carrierMergedNetwork_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_invalidNetwork_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID) + } + + getNetworkCallback() + .onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_nullSsid_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.ssid).thenReturn(null) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_unknownSsid_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.ssid).thenReturn(UNKNOWN_SSID) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_validSsid_true() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.ssid).thenReturn("FakeSsid") + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_activeToInactive_trueToFalse() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + // Start with active + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.ssid).thenReturn("FakeSsid") + } + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue() + + // WHEN the network is lost + getNetworkCallback().onLost(NETWORK) + + // THEN the isWifiConnected updates + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test fun wifiActivity_callbackGivesNone_activityFlowHasNone() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: DataActivityModel? = null val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) @@ -1111,7 +1248,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesIn_activityFlowHasIn() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: DataActivityModel? = null val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) @@ -1125,7 +1262,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesOut_activityFlowHasOut() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: DataActivityModel? = null val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) @@ -1139,7 +1276,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: DataActivityModel? = null val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) @@ -1159,7 +1296,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { logger, tableLogger, executor, - scope, + testScope.backgroundScope, wifiManager, ) } @@ -1204,5 +1341,3 @@ class WifiRepositoryImplTest : SysuiTestCase() { } } } - -private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt index ab4e93ceee84..4e0c309512e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.wifi.shared.model +import android.net.wifi.WifiManager.UNKNOWN_SSID import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -50,6 +51,42 @@ class WifiNetworkModelTest : SysuiTestCase() { WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1) } + @Test + fun active_hasValidSsid_nullSsid_false() { + val network = + WifiNetworkModel.Active( + NETWORK_ID, + level = MAX_VALID_LEVEL, + ssid = null, + ) + + assertThat(network.hasValidSsid()).isFalse() + } + + @Test + fun active_hasValidSsid_unknownSsid_false() { + val network = + WifiNetworkModel.Active( + NETWORK_ID, + level = MAX_VALID_LEVEL, + ssid = UNKNOWN_SSID, + ) + + assertThat(network.hasValidSsid()).isFalse() + } + + @Test + fun active_hasValidSsid_validSsid_true() { + val network = + WifiNetworkModel.Active( + NETWORK_ID, + level = MAX_VALID_LEVEL, + ssid = "FakeSsid", + ) + + assertThat(network.hasValidSsid()).isTrue() + } + // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java index 1eee08c22187..91c88cebff79 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java @@ -21,6 +21,7 @@ import static android.os.BatteryManager.EXTRA_PRESENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; @@ -167,8 +168,10 @@ public class BatteryControllerTest extends SysuiTestCase { mBatteryController.setPowerSaveMode(false, mView); StaticInOrder inOrder = inOrder(staticMockMarker(BatterySaverUtils.class)); - inOrder.verify(() -> BatterySaverUtils.setPowerSaveMode(getContext(), true, true)); - inOrder.verify(() -> BatterySaverUtils.setPowerSaveMode(getContext(), false, true)); + inOrder.verify(() -> BatterySaverUtils.setPowerSaveMode(getContext(), true, true, + SAVER_ENABLED_QS)); + inOrder.verify(() -> BatterySaverUtils.setPowerSaveMode(getContext(), false, true, + SAVER_ENABLED_QS)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 01e94baab7c3..391c8ca4d286 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -62,7 +62,7 @@ import android.window.OnBackInvokedDispatcher; import android.window.WindowOnBackInvokedDispatcher; import androidx.annotation.NonNull; -import androidx.core.animation.AnimatorTestRule2; +import androidx.core.animation.AnimatorTestRule; import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; @@ -110,7 +110,7 @@ public class RemoteInputViewTest extends SysuiTestCase { private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); @ClassRule - public static AnimatorTestRule2 mAnimatorTestRule = new AnimatorTestRule2(); + public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); @Before public void setUp() throws Exception { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt new file mode 100644 index 000000000000..c08ecd0e3b0c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import com.android.keyguard.FaceAuthUiEvent +import com.android.systemui.keyguard.shared.model.AuthenticationStatus +import com.android.systemui.keyguard.shared.model.DetectionStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository { + + override val isAuthenticated = MutableStateFlow(false) + override val canRunFaceAuth = MutableStateFlow(false) + private val _authenticationStatus = MutableStateFlow<AuthenticationStatus?>(null) + override val authenticationStatus: Flow<AuthenticationStatus> = + _authenticationStatus.filterNotNull() + fun setAuthenticationStatus(status: AuthenticationStatus) { + _authenticationStatus.value = status + } + private val _detectionStatus = MutableStateFlow<DetectionStatus?>(null) + override val detectionStatus: Flow<DetectionStatus> + get() = _detectionStatus.filterNotNull() + fun setDetectionStatus(status: DetectionStatus) { + _detectionStatus.value = status + } + override val isLockedOut = MutableStateFlow(false) + private val _runningAuthRequest = MutableStateFlow<Pair<FaceAuthUiEvent, Boolean>?>(null) + val runningAuthRequest: StateFlow<Pair<FaceAuthUiEvent, Boolean>?> = + _runningAuthRequest.asStateFlow() + override val isAuthRunning = _runningAuthRequest.map { it != null } + override val isBypassEnabled = MutableStateFlow(false) + + override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) { + _runningAuthRequest.value = uiEvent to fallbackToDetection + } + + override fun cancel() { + _runningAuthRequest.value = null + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt new file mode 100644 index 000000000000..9383a0a68844 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import android.content.Context +import com.android.systemui.plugins.qs.QSFactory +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.plugins.qs.QSTileView + +class FakeQSFactory(private val tileCreator: (String) -> QSTile?) : QSFactory { + override fun createTile(tileSpec: String): QSTile? { + return tileCreator(tileSpec) + } + + override fun createTileView( + context: Context?, + tile: QSTile?, + collapsedView: Boolean + ): QSTileView { + throw NotImplementedError("Not implemented") + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt new file mode 100644 index 000000000000..777130409aad --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.data.repository + +import android.content.ComponentName + +class FakeCustomTileAddedRepository : CustomTileAddedRepository { + + private val tileAddedRegistry = mutableSetOf<Pair<Int, ComponentName>>() + + override fun isTileAdded(componentName: ComponentName, userId: Int): Boolean { + return (userId to componentName) in tileAddedRegistry + } + + override fun setTileAdded(componentName: ComponentName, userId: Int, added: Boolean) { + if (added) { + tileAddedRegistry.add(userId to componentName) + } else { + tileAddedRegistry.remove(userId to componentName) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt new file mode 100644 index 000000000000..2865710c2eae --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.data.repository + +import android.util.Log +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END +import com.android.systemui.qs.pipeline.shared.TileSpec +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeTileSpecRepository : TileSpecRepository { + + private val tilesPerUser = mutableMapOf<Int, MutableStateFlow<List<TileSpec>>>() + + override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> { + return getFlow(userId).asStateFlow().also { Log.d("Fabian", "Retrieving flow for $userId") } + } + + override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) { + if (tile == TileSpec.Invalid) return + with(getFlow(userId)) { + value = + value.toMutableList().apply { + if (position == POSITION_AT_END) { + add(tile) + } else { + add(position, tile) + } + } + } + } + + override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) { + with(getFlow(userId)) { + value = + value.toMutableList().apply { removeAll(tiles.filter { it != TileSpec.Invalid }) } + } + } + + override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) { + getFlow(userId).value = tiles.filter { it != TileSpec.Invalid } + } + + private fun getFlow(userId: Int): MutableStateFlow<List<TileSpec>> = + tilesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) } +} diff --git a/services/Android.bp b/services/Android.bp index 6e6c55325e3d..b0a0e5e44a8c 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -112,6 +112,7 @@ filegroup { ":services.searchui-sources", ":services.selectiontoolbar-sources", ":services.smartspace-sources", + ":services.soundtrigger-sources", ":services.systemcaptions-sources", ":services.translation-sources", ":services.texttospeech-sources", @@ -169,6 +170,7 @@ java_library { "services.searchui", "services.selectiontoolbar", "services.smartspace", + "services.soundtrigger", "services.systemcaptions", "services.translation", "services.texttospeech", diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 8d039fc02026..2a964b8b701f 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -2073,15 +2073,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @Override public void onSaveRequestSuccess(@NonNull String servicePackageName, @Nullable IntentSender intentSender) { - // Log onSaveRequest result. - mSaveEventLogger.maybeSetIsSaved(true); - final long saveRequestFinishTimestamp = SystemClock.elapsedRealtime() - mLatencyBaseTime; - mSaveEventLogger.maybeSetLatencySaveFinishMillis(saveRequestFinishTimestamp); - mSaveEventLogger.logAndEndEvent(); - synchronized (mLock) { mSessionFlags.mShowingSaveUi = false; - + // Log onSaveRequest result. + mSaveEventLogger.maybeSetIsSaved(true); + final long saveRequestFinishTimestamp = + SystemClock.elapsedRealtime() - mLatencyBaseTime; + mSaveEventLogger.maybeSetLatencySaveFinishMillis(saveRequestFinishTimestamp); + mSaveEventLogger.logAndEndEvent(); if (mDestroyed) { Slog.w(TAG, "Call to Session#onSaveRequestSuccess() rejected - session: " + id + " destroyed"); @@ -2108,14 +2107,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @NonNull String servicePackageName) { boolean showMessage = !TextUtils.isEmpty(message); - // Log onSaveRequest result. - final long saveRequestFinishTimestamp = SystemClock.elapsedRealtime() - mLatencyBaseTime; - mSaveEventLogger.maybeSetLatencySaveFinishMillis(saveRequestFinishTimestamp); - mSaveEventLogger.logAndEndEvent(); - synchronized (mLock) { mSessionFlags.mShowingSaveUi = false; - + // Log onSaveRequest result. + final long saveRequestFinishTimestamp = + SystemClock.elapsedRealtime() - mLatencyBaseTime; + mSaveEventLogger.maybeSetLatencySaveFinishMillis(saveRequestFinishTimestamp); + mSaveEventLogger.logAndEndEvent(); if (mDestroyed) { Slog.w(TAG, "Call to Session#onSaveRequestFailure() rejected - session: " + id + " destroyed"); @@ -2228,8 +2226,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // AutoFillUiCallback @Override public void save() { - mSaveEventLogger.maybeSetSaveButtonClicked(true); synchronized (mLock) { + mSaveEventLogger.maybeSetSaveButtonClicked(true); if (mDestroyed) { Slog.w(TAG, "Call to Session#save() rejected - session: " + id + " destroyed"); @@ -2247,10 +2245,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // AutoFillUiCallback @Override public void cancelSave() { - mSaveEventLogger.maybeSetDialogDismissed(true); synchronized (mLock) { mSessionFlags.mShowingSaveUi = false; - + mSaveEventLogger.maybeSetDialogDismissed(true); if (mDestroyed) { Slog.w(TAG, "Call to Session#cancelSave() rejected - session: " + id + " destroyed"); diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 542cc2f0a0a6..5b320a87d113 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -19,6 +19,7 @@ package com.android.server.companion; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; +import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; import static android.content.pm.PackageManager.CERT_INPUT_SHA256; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; @@ -1066,6 +1067,10 @@ public class CompanionDeviceManagerService extends SystemService { // No role was granted to for this association, there is nothing else we need to here. return true; } + // Do not need to remove the system role since it was pre-granted by the system. + if (deviceProfile.equals(DEVICE_PROFILE_AUTOMOTIVE_PROJECTION)) { + return true; + } // Check if the applications is associated with another devices with the profile. If so, // it should remain the role holder. diff --git a/services/core/Android.bp b/services/core/Android.bp index c8caab93d76c..cfdf3ac5915b 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -174,7 +174,6 @@ java_library_static { "android.hardware.configstore-V1.1-java", "android.hardware.ir-V1-java", "android.hardware.rebootescrow-V1-java", - "android.hardware.soundtrigger-V2.3-java", "android.hardware.power.stats-V2-java", "android.hardware.power-V4-java", "android.hidl.manager-V1.2-java", diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java b/services/core/java/com/android/server/SoundTriggerInternal.java index cc398d930c7e..e6c1750c4a1d 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java +++ b/services/core/java/com/android/server/SoundTriggerInternal.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.soundtrigger; +package com.android.server; import android.annotation.NonNull; import android.annotation.Nullable; @@ -29,15 +29,13 @@ import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; import android.media.permission.Identity; import android.os.IBinder; -import com.android.server.voiceinteraction.VoiceInteractionManagerService; - import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.List; /** * Provides a local service for managing voice-related recoginition models. This is primarily used - * by the {@link VoiceInteractionManagerService}. + * by the {@code VoiceInteractionManagerService}. */ public interface SoundTriggerInternal { /** diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 7b618b11bd45..e248007eca19 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -26,6 +26,17 @@ import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP; import static android.app.ActivityManager.PROCESS_STATE_HEAVY_WEIGHT; import static android.app.ActivityManager.PROCESS_STATE_RECEIVER; import static android.app.ActivityManager.PROCESS_STATE_TOP; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BIND_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_COMPONENT_DISABLED; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_EXECUTING_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_NONE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_TASK; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHORT_FGS_TIMEOUT; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_STOP_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UID_IDLE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UNBIND_SERVICE; import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_DEPRECATED; import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_DISABLED; import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_OK; @@ -117,6 +128,7 @@ import android.annotation.UptimeMillisLong; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.ActivityManagerInternal.OomAdjReason; import android.app.ActivityManagerInternal.ServiceNotificationPolicy; import android.app.ActivityThread; import android.app.AppGlobals; @@ -1146,7 +1158,7 @@ public final class ActiveServices { } finally { /* Will be a no-op if nothing pending */ mAm.updateOomAdjPendingTargetsLocked( - OomAdjuster.OOM_ADJ_REASON_START_SERVICE); + OOM_ADJ_REASON_START_SERVICE); } } else { unbindServiceLocked(connection); @@ -1236,8 +1248,7 @@ public final class ActiveServices { /* ignore - local call */ } finally { /* Will be a no-op if nothing pending */ - mAm.updateOomAdjPendingTargetsLocked( - OomAdjuster.OOM_ADJ_REASON_START_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE); } } else { // Starting a service try { @@ -1311,7 +1322,7 @@ public final class ActiveServices { false /* packageFrozen */, true /* enqueueOomAdj */); /* Will be a no-op if nothing pending */ - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_START_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE); if (error != null) { return new ComponentName("!!", error); } @@ -1496,7 +1507,7 @@ public final class ActiveServices { stopServiceLocked(service, true); } if (size > 0) { - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_UID_IDLE); } } } @@ -3296,7 +3307,7 @@ public final class ActiveServices { Slog.e(TAG_SERVICE, "Short FGS procstate demoted: " + sr); - mAm.updateOomAdjLocked(sr.app, OomAdjuster.OOM_ADJ_REASON_SHORT_FGS_TIMEOUT); + mAm.updateOomAdjLocked(sr.app, OOM_ADJ_REASON_SHORT_FGS_TIMEOUT); } } @@ -3630,7 +3641,7 @@ public final class ActiveServices { needOomAdj = true; if (bringUpServiceLocked(s, service.getFlags(), callerFg, false, permissionsReviewRequired, packageFrozen, true) != null) { - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_BIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_BIND_SERVICE); return 0; } } @@ -3655,7 +3666,7 @@ public final class ActiveServices { mAm.enqueueOomAdjTargetLocked(s.app); } if (needOomAdj) { - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_BIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_BIND_SERVICE); } final int packageState = wasStopped @@ -3787,7 +3798,8 @@ public final class ActiveServices { } } - serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false, false); + serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false, false, + OOM_ADJ_REASON_EXECUTING_SERVICE); } } finally { Binder.restoreCallingIdentity(origId); @@ -3878,7 +3890,7 @@ public final class ActiveServices { } } - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_UNBIND_SERVICE); } finally { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); @@ -3925,7 +3937,8 @@ public final class ActiveServices { } } - serviceDoneExecutingLocked(r, inDestroying, false, false); + serviceDoneExecutingLocked(r, inDestroying, false, false, + OOM_ADJ_REASON_UNBIND_SERVICE); } } finally { Binder.restoreCallingIdentity(origId); @@ -4360,11 +4373,11 @@ public final class ActiveServices { /** * Bump the given service record into executing state. * @param oomAdjReason The caller requests it to perform the oomAdjUpdate not {@link - * OomAdjuster#OOM_ADJ_REASON_NONE}. + * ActivityManagerInternal#OOM_ADJ_REASON_NONE}. * @return {@code true} if it performed oomAdjUpdate. */ private boolean bumpServiceExecutingLocked( - ServiceRecord r, boolean fg, String why, @OomAdjuster.OomAdjReason int oomAdjReason) { + ServiceRecord r, boolean fg, String why, @OomAdjReason int oomAdjReason) { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, ">>> EXECUTING " + why + " of " + r + " in app " + r.app); else if (DEBUG_SERVICE_EXECUTING) Slog.v(TAG_SERVICE_EXECUTING, ">>> EXECUTING " @@ -4416,7 +4429,7 @@ public final class ActiveServices { } } boolean oomAdjusted = false; - if (oomAdjReason != OomAdjuster.OOM_ADJ_REASON_NONE && r.app != null + if (oomAdjReason != OOM_ADJ_REASON_NONE && r.app != null && r.app.mState.getCurProcState() > ActivityManager.PROCESS_STATE_SERVICE) { // Force an immediate oomAdjUpdate, so the client app could be in the correct process // state before doing any service related transactions @@ -4440,8 +4453,7 @@ public final class ActiveServices { + " rebind=" + rebind); if ((!i.requested || rebind) && i.apps.size() > 0) { try { - bumpServiceExecutingLocked(r, execInFg, "bind", - OomAdjuster.OOM_ADJ_REASON_BIND_SERVICE); + bumpServiceExecutingLocked(r, execInFg, "bind", OOM_ADJ_REASON_BIND_SERVICE); if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "requestServiceBinding=" + i.intent.getIntent() + ". bindSeq=" + mBindServiceSeqCounter); @@ -4457,13 +4469,15 @@ public final class ActiveServices { // Keep the executeNesting count accurate. if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Crashed while binding " + r, e); final boolean inDestroying = mDestroyingServices.contains(r); - serviceDoneExecutingLocked(r, inDestroying, inDestroying, false); + serviceDoneExecutingLocked(r, inDestroying, inDestroying, false, + OOM_ADJ_REASON_UNBIND_SERVICE); throw e; } catch (RemoteException e) { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Crashed while binding " + r); // Keep the executeNesting count accurate. final boolean inDestroying = mDestroyingServices.contains(r); - serviceDoneExecutingLocked(r, inDestroying, inDestroying, false); + serviceDoneExecutingLocked(r, inDestroying, inDestroying, false, + OOM_ADJ_REASON_UNBIND_SERVICE); return false; } } @@ -4841,7 +4855,7 @@ public final class ActiveServices { // Ignore, it's been logged and nothing upstack cares. } finally { /* Will be a no-op if nothing pending */ - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_START_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE); } } @@ -5193,13 +5207,14 @@ public final class ActiveServices { final ProcessServiceRecord psr = app.mServices; final boolean newService = psr.startService(r); - bumpServiceExecutingLocked(r, execInFg, "create", OomAdjuster.OOM_ADJ_REASON_NONE); + bumpServiceExecutingLocked(r, execInFg, "create", + OOM_ADJ_REASON_NONE /* use "none" to avoid extra oom adj */); mAm.updateLruProcessLocked(app, false, null); updateServiceForegroundLocked(psr, /* oomAdj= */ false); // Force an immediate oomAdjUpdate, so the client app could be in the correct process state // before doing any service related transactions mAm.enqueueOomAdjTargetLocked(app); - mAm.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_START_SERVICE); + mAm.updateOomAdjLocked(app, OOM_ADJ_REASON_START_SERVICE); boolean created = false; try { @@ -5233,7 +5248,8 @@ public final class ActiveServices { if (!created) { // Keep the executeNesting count accurate. final boolean inDestroying = mDestroyingServices.contains(r); - serviceDoneExecutingLocked(r, inDestroying, inDestroying, false); + serviceDoneExecutingLocked(r, inDestroying, inDestroying, false, + OOM_ADJ_REASON_STOP_SERVICE); // Cleanup. if (newService) { @@ -5319,7 +5335,8 @@ public final class ActiveServices { mAm.grantImplicitAccess(r.userId, si.intent, si.callingId, UserHandle.getAppId(r.appInfo.uid) ); - bumpServiceExecutingLocked(r, execInFg, "start", OomAdjuster.OOM_ADJ_REASON_NONE); + bumpServiceExecutingLocked(r, execInFg, "start", + OOM_ADJ_REASON_NONE /* use "none" to avoid extra oom adj */); if (r.fgRequired && !r.fgWaiting) { if (!r.isForeground) { if (DEBUG_BACKGROUND_CHECK) { @@ -5345,7 +5362,7 @@ public final class ActiveServices { if (!oomAdjusted) { mAm.enqueueOomAdjTargetLocked(r.app); - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_START_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE); } ParceledListSlice<ServiceStartArgs> slice = new ParceledListSlice<>(args); slice.setInlineCountLimit(4); @@ -5371,10 +5388,11 @@ public final class ActiveServices { // Keep nesting count correct final boolean inDestroying = mDestroyingServices.contains(r); for (int i = 0, size = args.size(); i < size; i++) { - serviceDoneExecutingLocked(r, inDestroying, inDestroying, true); + serviceDoneExecutingLocked(r, inDestroying, inDestroying, true, + OOM_ADJ_REASON_STOP_SERVICE); } /* Will be a no-op if nothing pending */ - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_STOP_SERVICE); if (caughtException instanceof TransactionTooLargeException) { throw (TransactionTooLargeException)caughtException; } @@ -5461,7 +5479,7 @@ public final class ActiveServices { if (ibr.hasBound) { try { oomAdjusted |= bumpServiceExecutingLocked(r, false, "bring down unbind", - OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + OOM_ADJ_REASON_UNBIND_SERVICE); ibr.hasBound = false; ibr.requested = false; r.app.getThread().scheduleUnbindService(r, @@ -5615,7 +5633,7 @@ public final class ActiveServices { } else { try { oomAdjusted |= bumpServiceExecutingLocked(r, false, "destroy", - oomAdjusted ? 0 : OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + oomAdjusted ? 0 : OOM_ADJ_REASON_STOP_SERVICE); mDestroyingServices.add(r); r.destroying = true; r.app.getThread().scheduleStopService(r); @@ -5637,7 +5655,7 @@ public final class ActiveServices { if (!oomAdjusted) { mAm.enqueueOomAdjTargetLocked(r.app); if (!enqueueOomAdj) { - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_STOP_SERVICE); } } if (r.bindings.size() > 0) { @@ -5762,8 +5780,7 @@ public final class ActiveServices { if (s.app != null && s.app.getThread() != null && b.intent.apps.size() == 0 && b.intent.hasBound) { try { - bumpServiceExecutingLocked(s, false, "unbind", - OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + bumpServiceExecutingLocked(s, false, "unbind", OOM_ADJ_REASON_UNBIND_SERVICE); if (b.client != s.app && c.notHasFlag(Context.BIND_WAIVE_PRIORITY) && s.app.mState.getSetProcState() <= PROCESS_STATE_HEAVY_WEIGHT) { // If this service's process is not already in the cached list, @@ -5886,7 +5903,8 @@ public final class ActiveServices { } } final long origId = Binder.clearCallingIdentity(); - serviceDoneExecutingLocked(r, inDestroying, inDestroying, enqueueOomAdj); + serviceDoneExecutingLocked(r, inDestroying, inDestroying, enqueueOomAdj, + OOM_ADJ_REASON_EXECUTING_SERVICE); Binder.restoreCallingIdentity(origId); } else { Slog.w(TAG, "Done executing unknown service from pid " @@ -5905,11 +5923,11 @@ public final class ActiveServices { r.tracker.setStarted(false, memFactor, now); } } - serviceDoneExecutingLocked(r, true, true, enqueueOomAdj); + serviceDoneExecutingLocked(r, true, true, enqueueOomAdj, OOM_ADJ_REASON_PROCESS_END); } private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying, - boolean finishing, boolean enqueueOomAdj) { + boolean finishing, boolean enqueueOomAdj, @OomAdjReason int oomAdjReason) { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "<<< DONE EXECUTING " + r + ": nesting=" + r.executeNesting + ", inDestroying=" + inDestroying + ", app=" + r.app); @@ -5945,7 +5963,7 @@ public final class ActiveServices { if (enqueueOomAdj) { mAm.enqueueOomAdjTargetLocked(r.app); } else { - mAm.updateOomAdjLocked(r.app, OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjLocked(r.app, oomAdjReason); } } r.executeFg = false; @@ -6015,7 +6033,7 @@ public final class ActiveServices { bringDownServiceLocked(sr, true); } /* Will be a no-op if nothing pending */ - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_START_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE); } } catch (RemoteException e) { Slog.w(TAG, "Exception in new application when starting service " @@ -6075,7 +6093,7 @@ public final class ActiveServices { } } if (needOomAdj) { - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_PROCESS_END); } } @@ -6146,7 +6164,7 @@ public final class ActiveServices { bringDownServiceLocked(mTmpCollectionResults.get(i), true); } if (size > 0) { - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_COMPONENT_DISABLED); } if (fullStop && !mTmpCollectionResults.isEmpty()) { // if we're tearing down the app's entire service state, account for possible @@ -6273,7 +6291,7 @@ public final class ActiveServices { } } if (needOomAdj) { - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_REMOVE_TASK); } } @@ -6444,7 +6462,7 @@ public final class ActiveServices { } } - mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_STOP_SERVICE); if (!allowRestart) { psr.stopAllServices(); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ef7d5ae43396..4a6c9b257b9a 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -44,6 +44,14 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BACKUP; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHELL; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SYSTEM_INIT; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UI_VISIBILITY; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.OP_NONE; import static android.content.pm.ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT; @@ -199,6 +207,7 @@ import android.app.ActivityManagerInternal.BindServiceEventListener; import android.app.ActivityManagerInternal.BroadcastEventListener; import android.app.ActivityManagerInternal.ForegroundServiceStateListener; import android.app.ActivityManagerInternal.MediaProjectionTokenEvent; +import android.app.ActivityManagerInternal.OomAdjReason; import android.app.ActivityTaskManager.RootTaskInfo; import android.app.ActivityThread; import android.app.AnrController; @@ -368,7 +377,6 @@ import android.util.FeatureFlagUtils; import android.util.IndentingPrintWriter; import android.util.IntArray; import android.util.Log; -import android.util.LogWriter; import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Slog; @@ -1968,7 +1976,7 @@ public class ActivityManagerService extends IActivityManager.Stub app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_SYSTEM); addPidLocked(app); updateLruProcessLocked(app, false, null); - updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + updateOomAdjLocked(OOM_ADJ_REASON_SYSTEM_INIT); } } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException( @@ -2502,7 +2510,7 @@ public class ActivityManagerService extends IActivityManager.Stub // bind background threads to little cores // this is expected to fail inside of framework tests because apps can't touch cpusets directly // make sure we've already adjusted system_server's internal view of itself first - updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + updateOomAdjLocked(OOM_ADJ_REASON_SYSTEM_INIT); try { Process.setThreadGroupAndCpuset(BackgroundThread.get().getThreadId(), Process.THREAD_GROUP_SYSTEM); @@ -3387,7 +3395,7 @@ public class ActivityManagerService extends IActivityManager.Stub handleAppDiedLocked(app, pid, false, true, fromBinderDied); if (doOomAdj) { - updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_PROCESS_END); + updateOomAdjLocked(OOM_ADJ_REASON_PROCESS_END); } if (doLowMem) { mAppProfiler.doLowMemReportIfNeededLocked(app); @@ -4843,7 +4851,7 @@ public class ActivityManagerService extends IActivityManager.Stub } if (!didSomething) { - updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN); + updateOomAdjLocked(app, OOM_ADJ_REASON_PROCESS_BEGIN); checkTime(startTime, "finishAttachApplicationInner: after updateOomAdjLocked"); } @@ -5485,7 +5493,7 @@ public class ActivityManagerService extends IActivityManager.Stub "setProcessLimit()"); synchronized (this) { mConstants.setOverrideMaxCachedProcesses(max); - trimApplicationsLocked(true, OomAdjuster.OOM_ADJ_REASON_PROCESS_END); + trimApplicationsLocked(true, OOM_ADJ_REASON_PROCESS_END); } } @@ -5513,7 +5521,7 @@ public class ActivityManagerService extends IActivityManager.Stub pr.mState.setForcingToImportant(null); clearProcessForegroundLocked(pr); } - updateOomAdjLocked(pr, OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY); + updateOomAdjLocked(pr, OOM_ADJ_REASON_UI_VISIBILITY); } } @@ -5560,7 +5568,7 @@ public class ActivityManagerService extends IActivityManager.Stub } if (changed) { - updateOomAdjLocked(pr, OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY); + updateOomAdjLocked(pr, OOM_ADJ_REASON_UI_VISIBILITY); } } } @@ -6869,7 +6877,7 @@ public class ActivityManagerService extends IActivityManager.Stub new HostingRecord(HostingRecord.HOSTING_TYPE_ADDED_APPLICATION, customProcess != null ? customProcess : info.processName)); updateLruProcessLocked(app, false, null); - updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN); + updateOomAdjLocked(app, OOM_ADJ_REASON_PROCESS_BEGIN); } // Report usage as process is persistent and being started. @@ -6986,7 +6994,7 @@ public class ActivityManagerService extends IActivityManager.Stub mOomAdjProfiler.onWakefulnessChanged(wakefulness); mOomAdjuster.onWakefulnessChanged(wakefulness); - updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY); + updateOomAdjLocked(OOM_ADJ_REASON_UI_VISIBILITY); } } } @@ -7748,7 +7756,7 @@ public class ActivityManagerService extends IActivityManager.Stub } } if (changed) { - updateOomAdjLocked(pr, OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY); + updateOomAdjLocked(pr, OOM_ADJ_REASON_UI_VISIBILITY); } } } finally { @@ -9508,7 +9516,7 @@ public class ActivityManagerService extends IActivityManager.Stub mAppProfiler.setMemFactorOverrideLocked(level); // Kick off an oom adj update since we forced a mem factor update. - updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + updateOomAdjLocked(OOM_ADJ_REASON_SHELL); } } @@ -13408,7 +13416,7 @@ public class ActivityManagerService extends IActivityManager.Stub proc.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_BACKUP); // Try not to kill the process during backup - updateOomAdjLocked(proc, OomAdjuster.OOM_ADJ_REASON_NONE); + updateOomAdjLocked(proc, OOM_ADJ_REASON_BACKUP); // If the process is already attached, schedule the creation of the backup agent now. // If it is not yet live, this will be done when it attaches to the framework. @@ -13532,7 +13540,7 @@ public class ActivityManagerService extends IActivityManager.Stub // Not backing this app up any more; reset its OOM adjustment final ProcessRecord proc = backupTarget.app; - updateOomAdjLocked(proc, OomAdjuster.OOM_ADJ_REASON_NONE); + updateOomAdjLocked(proc, OOM_ADJ_REASON_BACKUP); proc.setInFullBackup(false); proc.mProfile.clearHostingComponentType(HOSTING_COMPONENT_TYPE_BACKUP); @@ -13713,7 +13721,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (!sdkSandboxManagerLocal.canRegisterBroadcastReceiver( /*IntentFilter=*/ filter, flags, onlyProtectedBroadcasts)) { throw new SecurityException("SDK sandbox not allowed to register receiver" - + " with the given IntentFilter: " + filter.toString()); + + " with the given IntentFilter: " + filter.toLongString()); } } @@ -13923,7 +13931,7 @@ public class ActivityManagerService extends IActivityManager.Stub // If we actually concluded any broadcasts, we might now be able // to trim the recipients' apps from our working set if (doTrim) { - trimApplicationsLocked(false, OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER); + trimApplicationsLocked(false, OOM_ADJ_REASON_FINISH_RECEIVER); return; } } @@ -15185,7 +15193,7 @@ public class ActivityManagerService extends IActivityManager.Stub queue.finishReceiverLocked(callerApp, resultCode, resultData, resultExtras, resultAbort, true); // updateOomAdjLocked() will be done here - trimApplicationsLocked(false, OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER); + trimApplicationsLocked(false, OOM_ADJ_REASON_FINISH_RECEIVER); } } finally { @@ -16130,7 +16138,7 @@ public class ActivityManagerService extends IActivityManager.Stub item.foregroundServiceTypes = fgServiceTypes; } if (oomAdj) { - updateOomAdjLocked(proc, OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY); + updateOomAdjLocked(proc, OOM_ADJ_REASON_UI_VISIBILITY); } } @@ -16196,7 +16204,7 @@ public class ActivityManagerService extends IActivityManager.Stub * {@link #enqueueOomAdjTargetLocked}. */ @GuardedBy("this") - void updateOomAdjPendingTargetsLocked(@OomAdjuster.OomAdjReason int oomAdjReason) { + void updateOomAdjPendingTargetsLocked(@OomAdjReason int oomAdjReason) { mOomAdjuster.updateOomAdjPendingTargetsLocked(oomAdjReason); } @@ -16215,7 +16223,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @GuardedBy("this") - final void updateOomAdjLocked(@OomAdjuster.OomAdjReason int oomAdjReason) { + final void updateOomAdjLocked(@OomAdjReason int oomAdjReason) { mOomAdjuster.updateOomAdjLocked(oomAdjReason); } @@ -16227,8 +16235,7 @@ public class ActivityManagerService extends IActivityManager.Stub * @return whether updateOomAdjLocked(app) was successful. */ @GuardedBy("this") - final boolean updateOomAdjLocked( - ProcessRecord app, @OomAdjuster.OomAdjReason int oomAdjReason) { + final boolean updateOomAdjLocked(ProcessRecord app, @OomAdjReason int oomAdjReason) { return mOomAdjuster.updateOomAdjLocked(app, oomAdjReason); } @@ -16461,16 +16468,14 @@ public class ActivityManagerService extends IActivityManager.Stub mOomAdjuster.setUidTempAllowlistStateLSP(uid, onAllowlist); } - private void trimApplications( - boolean forceFullOomAdj, @OomAdjuster.OomAdjReason int oomAdjReason) { + private void trimApplications(boolean forceFullOomAdj, @OomAdjReason int oomAdjReason) { synchronized (this) { trimApplicationsLocked(forceFullOomAdj, oomAdjReason); } } @GuardedBy("this") - private void trimApplicationsLocked( - boolean forceFullOomAdj, @OomAdjuster.OomAdjReason int oomAdjReason) { + private void trimApplicationsLocked(boolean forceFullOomAdj, @OomAdjReason int oomAdjReason) { // First remove any unused application processes whose package // has been removed. boolean didSomething = false; @@ -17442,7 +17447,7 @@ public class ActivityManagerService extends IActivityManager.Stub } pr.mState.setHasOverlayUi(hasOverlayUi); //Slog.i(TAG, "Setting hasOverlayUi=" + pr.hasOverlayUi + " for pid=" + pid); - updateOomAdjLocked(pr, OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY); + updateOomAdjLocked(pr, OOM_ADJ_REASON_UI_VISIBILITY); } } @@ -17577,7 +17582,7 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void trimApplications() { - ActivityManagerService.this.trimApplications(true, OomAdjuster.OOM_ADJ_REASON_ACTIVITY); + ActivityManagerService.this.trimApplications(true, OOM_ADJ_REASON_ACTIVITY); } public void killProcessesForRemovedTask(ArrayList<Object> procsToKill) { @@ -17626,9 +17631,9 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void updateOomAdj() { + public void updateOomAdj(@OomAdjReason int oomAdjReason) { synchronized (ActivityManagerService.this) { - ActivityManagerService.this.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + ActivityManagerService.this.updateOomAdjLocked(oomAdjReason); } } @@ -18288,8 +18293,7 @@ public class ActivityManagerService extends IActivityManager.Stub // sends to the activity. After this race issue between WM/ATMS and AMS is solved, this // workaround can be removed. (b/213288355) if (isNewPending) { - mOomAdjuster.mCachedAppOptimizer.unfreezeProcess(pid, - OomAdjuster.OOM_ADJ_REASON_ACTIVITY); + mOomAdjuster.mCachedAppOptimizer.unfreezeProcess(pid, OOM_ADJ_REASON_ACTIVITY); } // We need to update the network rules for the app coming to the top state so that // it can access network when the device or the app is in a restricted state diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index bfc8251d97bb..9e61ce405ca7 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -18,7 +18,6 @@ package com.android.server.am; import static com.android.internal.util.Preconditions.checkState; import static com.android.server.am.BroadcastRecord.deliveryStateToString; -import static com.android.server.am.BroadcastRecord.isDeliveryStateTerminal; import static com.android.server.am.BroadcastRecord.isReceiverEquals; import android.annotation.IntDef; @@ -709,7 +708,7 @@ class BroadcastProcessQueue { || consecutiveHighPriorityCount >= maxHighPriorityDispatchLimit); final boolean isLPQueueEligible = shouldConsiderLPQueue && nextLPRecord.enqueueTime <= nextHPRecord.enqueueTime - && !blockedOnOrderedDispatch(nextLPRecord, nextLPRecordIndex); + && !nextLPRecord.isBlocked(nextLPRecordIndex); return isLPQueueEligible ? lowPriorityQueue : highPriorityQueue; } @@ -912,39 +911,20 @@ class BroadcastProcessQueue { } } - private boolean blockedOnOrderedDispatch(BroadcastRecord r, int index) { - final int blockedUntilTerminalCount = r.blockedUntilTerminalCount[index]; - - int existingDeferredCount = 0; - if (r.deferUntilActive) { - for (int i = 0; i < index; i++) { - if (r.deferredUntilActive[i]) existingDeferredCount++; - } - } - - // We might be blocked waiting for other receivers to finish, - // typically for an ordered broadcast or priority traunches - if ((r.terminalCount + existingDeferredCount) < blockedUntilTerminalCount - && !isDeliveryStateTerminal(r.getDeliveryState(index))) { - return true; - } - return false; - } - /** * Update {@link #getRunnableAt()} if it's currently invalidated. */ private void updateRunnableAt() { - final SomeArgs next = peekNextBroadcast(); + if (!mRunnableAtInvalidated) return; mRunnableAtInvalidated = false; + + final SomeArgs next = peekNextBroadcast(); if (next != null) { final BroadcastRecord r = (BroadcastRecord) next.arg1; final int index = next.argi1; final long runnableAt = r.enqueueTime; - // If we're specifically queued behind other ordered dispatch activity, - // we aren't runnable yet - if (blockedOnOrderedDispatch(r, index)) { + if (r.isBlocked(index)) { mRunnableAt = Long.MAX_VALUE; mRunnableAtReason = REASON_BLOCKED; return; @@ -1262,12 +1242,12 @@ class BroadcastProcessQueue { pw.print(info.activityInfo.name); } pw.println(); - final int blockedUntilTerminalCount = record.blockedUntilTerminalCount[recordIndex]; - if (blockedUntilTerminalCount != -1) { + final int blockedUntilBeyondCount = record.blockedUntilBeyondCount[recordIndex]; + if (blockedUntilBeyondCount != -1) { pw.print(" blocked until "); - pw.print(blockedUntilTerminalCount); + pw.print(blockedUntilBeyondCount); pw.print(", currently at "); - pw.print(record.terminalCount); + pw.print(record.beyondCount); pw.print(" of "); pw.println(record.receivers.size()); } diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index bd36c3ff6f98..5a4d315767ca 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -17,6 +17,7 @@ package com.android.server.am; import static android.app.ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER; import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY; import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE; import static android.text.TextUtils.formatSimple; @@ -37,7 +38,6 @@ import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_L import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU; import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_BROADCAST; import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_MU; -import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_START_RECEIVER; import android.annotation.NonNull; import android.annotation.Nullable; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index a4bdf61e628f..e532c15addd0 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -16,6 +16,7 @@ package com.android.server.am; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER; import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY; import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE; @@ -38,7 +39,6 @@ import static com.android.server.am.BroadcastRecord.getReceiverPackageName; import static com.android.server.am.BroadcastRecord.getReceiverProcessName; import static com.android.server.am.BroadcastRecord.getReceiverUid; import static com.android.server.am.BroadcastRecord.isDeliveryStateTerminal; -import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_START_RECEIVER; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1008,6 +1008,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } final BroadcastRecord r = queue.getActive(); + final int index = queue.getActiveIndex(); if (r.ordered) { r.resultCode = resultCode; r.resultData = resultData; @@ -1015,18 +1016,24 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (!r.isNoAbort()) { r.resultAbort = resultAbort; } + } - // When the caller aborted an ordered broadcast, we mark all - // remaining receivers as skipped - if (r.resultAbort) { - for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) { - setDeliveryState(null, null, r, i, r.receivers.get(i), - BroadcastRecord.DELIVERY_SKIPPED, "resultAbort"); - } + // To ensure that "beyond" high-water marks are updated in a monotonic + // way, we finish this receiver before possibly skipping any remaining + // aborted receivers + final boolean res = finishReceiverActiveLocked(queue, + BroadcastRecord.DELIVERY_DELIVERED, "remote app"); + + // When the caller aborted an ordered broadcast, we mark all + // remaining receivers as skipped + if (r.resultAbort) { + for (int i = index + 1; i < r.receivers.size(); i++) { + setDeliveryState(null, null, r, i, r.receivers.get(i), + BroadcastRecord.DELIVERY_SKIPPED, "resultAbort"); } } - return finishReceiverActiveLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app"); + return res; } /** @@ -1108,21 +1115,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { @NonNull Object receiver, @DeliveryState int newDeliveryState, @NonNull String reason) { final int cookie = traceBegin("setDeliveryState"); + + // Remember the old state and apply the new state final int oldDeliveryState = getDeliveryState(r, index); - boolean checkFinished = false; - - // Only apply state when we haven't already reached a terminal state; - // this is how we ignore racing timeout messages - if (!isDeliveryStateTerminal(oldDeliveryState)) { - r.setDeliveryState(index, newDeliveryState, reason); - if (oldDeliveryState == BroadcastRecord.DELIVERY_DEFERRED) { - r.deferredCount--; - } else if (newDeliveryState == BroadcastRecord.DELIVERY_DEFERRED) { - // If we're deferring a broadcast, maybe that's enough to unblock the final callback - r.deferredCount++; - checkFinished = true; - } - } + final boolean beyondCountChanged = r.setDeliveryState(index, newDeliveryState, reason); // Emit any relevant tracing results when we're changing the delivery // state as part of running from a queue @@ -1147,15 +1143,13 @@ class BroadcastQueueModernImpl extends BroadcastQueue { + deliveryStateToString(newDeliveryState) + " because " + reason); } - r.terminalCount++; notifyFinishReceiver(queue, app, r, index, receiver); - checkFinished = true; } - // When entire ordered broadcast finished, deliver final result - if (checkFinished) { - final boolean recordFinished = - ((r.terminalCount + r.deferredCount) == r.receivers.size()); - if (recordFinished) { + + // When we've reached a new high-water mark, we might be in a position + // to unblock other receivers or the final resultTo + if (beyondCountChanged) { + if (r.beyondCount == r.receivers.size()) { scheduleResultTo(r); } diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index c368290386a0..64fe39314f0e 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -24,6 +24,7 @@ import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROA import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_NONE; import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_TARGET_T_ONLY; +import android.annotation.CheckResult; import android.annotation.CurrentTimeMillisLong; import android.annotation.ElapsedRealtimeLong; import android.annotation.IntDef; @@ -101,8 +102,7 @@ final class BroadcastRecord extends Binder { final @NonNull List<Object> receivers; // contains BroadcastFilter and ResolveInfo final @DeliveryState int[] delivery; // delivery state of each receiver final @NonNull String[] deliveryReasons; // reasons for delivery state of each receiver - final boolean[] deferredUntilActive; // whether each receiver is infinitely deferred - final int[] blockedUntilTerminalCount; // blocked until count of each receiver + final int[] blockedUntilBeyondCount; // blocked until count of each receiver @Nullable ProcessRecord resultToApp; // who receives final result if non-null @Nullable IIntentReceiver resultTo; // who receives final result if non-null boolean deferred; @@ -134,6 +134,7 @@ final class BroadcastRecord extends Binder { int manifestSkipCount; // number of manifest receivers skipped. int terminalCount; // number of receivers in terminal state. int deferredCount; // number of receivers in deferred state. + int beyondCount; // high-water number of receivers we've moved beyond. @Nullable BroadcastQueue queue; // the outbound queue handling this broadcast // Determines the privileges the app's process has in regard to background starts. @@ -219,6 +220,23 @@ final class BroadcastRecord extends Binder { } } + /** + * Return if the given delivery state is "beyond", which means that we've + * moved beyond this receiver, and future receivers are now unblocked. + */ + static boolean isDeliveryStateBeyond(@DeliveryState int deliveryState) { + switch (deliveryState) { + case DELIVERY_DELIVERED: + case DELIVERY_SKIPPED: + case DELIVERY_TIMEOUT: + case DELIVERY_FAILURE: + case DELIVERY_DEFERRED: + return true; + default: + return false; + } + } + ProcessRecord curApp; // hosting application of current receiver. ComponentName curComponent; // the receiver class that is currently running. ActivityInfo curReceiver; // the manifest receiver that is currently running. @@ -356,7 +374,7 @@ final class BroadcastRecord extends Binder { TimeUtils.formatDuration(terminalTime[i] - scheduledTime[i], pw); pw.print(' '); } - pw.print("("); pw.print(blockedUntilTerminalCount[i]); pw.print(") "); + pw.print("("); pw.print(blockedUntilBeyondCount[i]); pw.print(") "); pw.print("#"); pw.print(i); pw.print(": "); if (o instanceof BroadcastFilter) { pw.println(o); @@ -411,8 +429,7 @@ final class BroadcastRecord extends Binder { urgent = calculateUrgent(_intent, _options); deferUntilActive = calculateDeferUntilActive(_callingUid, _options, _resultTo, _serialized, urgent); - deferredUntilActive = new boolean[deferUntilActive ? delivery.length : 0]; - blockedUntilTerminalCount = calculateBlockedUntilTerminalCount(receivers, _serialized); + blockedUntilBeyondCount = calculateBlockedUntilBeyondCount(receivers, _serialized); scheduledTime = new long[delivery.length]; terminalTime = new long[delivery.length]; resultToApp = _resultToApp; @@ -423,7 +440,7 @@ final class BroadcastRecord extends Binder { ordered = _serialized; sticky = _sticky; initialSticky = _initialSticky; - prioritized = isPrioritized(blockedUntilTerminalCount, _serialized); + prioritized = isPrioritized(blockedUntilBeyondCount, _serialized); userId = _userId; nextReceiver = 0; state = IDLE; @@ -467,8 +484,7 @@ final class BroadcastRecord extends Binder { delivery = from.delivery; deliveryReasons = from.deliveryReasons; deferUntilActive = from.deferUntilActive; - deferredUntilActive = from.deferredUntilActive; - blockedUntilTerminalCount = from.blockedUntilTerminalCount; + blockedUntilBeyondCount = from.blockedUntilBeyondCount; scheduledTime = from.scheduledTime; terminalTime = from.terminalTime; resultToApp = from.resultToApp; @@ -627,32 +643,72 @@ final class BroadcastRecord extends Binder { /** * Update the delivery state of the given {@link #receivers} index. * Automatically updates any time measurements related to state changes. + * + * @return if {@link #beyondCount} changed due to this state transition, + * indicating that other events may be unblocked. */ - void setDeliveryState(int index, @DeliveryState int deliveryState, + @CheckResult + boolean setDeliveryState(int index, @DeliveryState int newDeliveryState, @NonNull String reason) { - delivery[index] = deliveryState; - deliveryReasons[index] = reason; - if (deferUntilActive) deferredUntilActive[index] = false; - switch (deliveryState) { - case DELIVERY_DELIVERED: - case DELIVERY_SKIPPED: - case DELIVERY_TIMEOUT: - case DELIVERY_FAILURE: - terminalTime[index] = SystemClock.uptimeMillis(); + final int oldDeliveryState = delivery[index]; + if (isDeliveryStateTerminal(oldDeliveryState) + || newDeliveryState == oldDeliveryState) { + // We've already arrived in terminal or requested state, so leave + // any statistics and reasons intact from the first transition + return false; + } + + switch (oldDeliveryState) { + case DELIVERY_DEFERRED: + deferredCount--; break; + } + switch (newDeliveryState) { case DELIVERY_SCHEDULED: scheduledTime[index] = SystemClock.uptimeMillis(); break; case DELIVERY_DEFERRED: - if (deferUntilActive) deferredUntilActive[index] = true; + deferredCount++; + break; + case DELIVERY_DELIVERED: + case DELIVERY_SKIPPED: + case DELIVERY_TIMEOUT: + case DELIVERY_FAILURE: + terminalTime[index] = SystemClock.uptimeMillis(); + terminalCount++; break; } + + delivery[index] = newDeliveryState; + deliveryReasons[index] = reason; + + // If this state change might bring us to a new high-water mark, bring + // ourselves as high as we possibly can + final int oldBeyondCount = beyondCount; + if (index >= beyondCount) { + for (int i = beyondCount; i < delivery.length; i++) { + if (isDeliveryStateBeyond(getDeliveryState(i))) { + beyondCount = i + 1; + } else { + break; + } + } + } + return (beyondCount != oldBeyondCount); } @DeliveryState int getDeliveryState(int index) { return delivery[index]; } + /** + * @return if the given {@link #receivers} index should be considered + * blocked based on the current status of the overall broadcast. + */ + boolean isBlocked(int index) { + return (beyondCount < blockedUntilBeyondCount[index]); + } + boolean wasDeliveryAttempted(int index) { final int deliveryState = getDeliveryState(index); switch (deliveryState) { @@ -757,36 +813,36 @@ final class BroadcastRecord extends Binder { * has prioritized tranches of receivers. */ @VisibleForTesting - static boolean isPrioritized(@NonNull int[] blockedUntilTerminalCount, + static boolean isPrioritized(@NonNull int[] blockedUntilBeyondCount, boolean ordered) { - return !ordered && (blockedUntilTerminalCount.length > 0) - && (blockedUntilTerminalCount[0] != -1); + return !ordered && (blockedUntilBeyondCount.length > 0) + && (blockedUntilBeyondCount[0] != -1); } /** - * Calculate the {@link #terminalCount} that each receiver should be + * Calculate the {@link #beyondCount} that each receiver should be * considered blocked until. * <p> * For example, in an ordered broadcast, receiver {@code N} is blocked until - * receiver {@code N-1} reaches a terminal state. Similarly, in a - * prioritized broadcast, receiver {@code N} is blocked until all receivers - * of a higher priority reach a terminal state. + * receiver {@code N-1} reaches a terminal or deferred state. Similarly, in + * a prioritized broadcast, receiver {@code N} is blocked until all + * receivers of a higher priority reach a terminal or deferred state. * <p> - * When there are no terminal count constraints, the blocked value for each + * When there are no beyond count constraints, the blocked value for each * receiver is {@code -1}. */ @VisibleForTesting - static @NonNull int[] calculateBlockedUntilTerminalCount( + static @NonNull int[] calculateBlockedUntilBeyondCount( @NonNull List<Object> receivers, boolean ordered) { final int N = receivers.size(); - final int[] blockedUntilTerminalCount = new int[N]; + final int[] blockedUntilBeyondCount = new int[N]; int lastPriority = 0; int lastPriorityIndex = 0; for (int i = 0; i < N; i++) { if (ordered) { // When sending an ordered broadcast, we need to block this // receiver until all previous receivers have terminated - blockedUntilTerminalCount[i] = i; + blockedUntilBeyondCount[i] = i; } else { // When sending a prioritized broadcast, we only need to wait // for the previous tranche of receivers to be terminated @@ -794,18 +850,18 @@ final class BroadcastRecord extends Binder { if ((i == 0) || (thisPriority != lastPriority)) { lastPriority = thisPriority; lastPriorityIndex = i; - blockedUntilTerminalCount[i] = i; + blockedUntilBeyondCount[i] = i; } else { - blockedUntilTerminalCount[i] = lastPriorityIndex; + blockedUntilBeyondCount[i] = lastPriorityIndex; } } } // If the entire list is in the same priority tranche, mark as -1 to // indicate that none of them need to wait - if (N > 0 && blockedUntilTerminalCount[N - 1] == 0) { - Arrays.fill(blockedUntilTerminalCount, -1); + if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) { + Arrays.fill(blockedUntilBeyondCount, -1); } - return blockedUntilTerminalCount; + return blockedUntilBeyondCount; } static int getReceiverUid(@NonNull Object receiver) { diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 568997bb2667..f42087ff8006 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -18,6 +18,28 @@ package com.android.server.am; import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN; import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ALLOWLIST; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BACKUP; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BIND_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_COMPONENT_DISABLED; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_EXECUTING_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_GET_PROVIDER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_PROVIDER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_TASK; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RESTRICTION_CHANGE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHELL; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHORT_FGS_TIMEOUT; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_STOP_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SYSTEM_INIT; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UID_IDLE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UI_VISIBILITY; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UNBIND_SERVICE; import static android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_COMPACTION; @@ -26,6 +48,7 @@ import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import android.annotation.IntDef; import android.app.ActivityManager; +import android.app.ActivityManagerInternal.OomAdjReason; import android.app.ActivityThread; import android.app.ApplicationExitInfo; import android.app.IApplicationThread; @@ -139,6 +162,26 @@ public final class CachedAppOptimizer { FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_BINDER_TXNS; static final int UNFREEZE_REASON_FEATURE_FLAGS = FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_FEATURE_FLAGS; + static final int UNFREEZE_REASON_SHORT_FGS_TIMEOUT = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_SHORT_FGS_TIMEOUT; + static final int UNFREEZE_REASON_SYSTEM_INIT = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_SYSTEM_INIT; + static final int UNFREEZE_REASON_BACKUP = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_BACKUP; + static final int UNFREEZE_REASON_SHELL = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_SHELL; + static final int UNFREEZE_REASON_REMOVE_TASK = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_REMOVE_TASK; + static final int UNFREEZE_REASON_UID_IDLE = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_UID_IDLE; + static final int UNFREEZE_REASON_STOP_SERVICE = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_STOP_SERVICE; + static final int UNFREEZE_REASON_EXECUTING_SERVICE = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_EXECUTING_SERVICE; + static final int UNFREEZE_REASON_RESTRICTION_CHANGE = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_RESTRICTION_CHANGE; + static final int UNFREEZE_REASON_COMPONENT_DISABLED = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_COMPONENT_DISABLED; @IntDef(prefix = {"UNFREEZE_REASON_"}, value = { UNFREEZE_REASON_NONE, @@ -160,6 +203,16 @@ public final class CachedAppOptimizer { UNFREEZE_REASON_FILE_LOCK_CHECK_FAILURE, UNFREEZE_REASON_BINDER_TXNS, UNFREEZE_REASON_FEATURE_FLAGS, + UNFREEZE_REASON_SHORT_FGS_TIMEOUT, + UNFREEZE_REASON_SYSTEM_INIT, + UNFREEZE_REASON_BACKUP, + UNFREEZE_REASON_SHELL, + UNFREEZE_REASON_REMOVE_TASK, + UNFREEZE_REASON_UID_IDLE, + UNFREEZE_REASON_STOP_SERVICE, + UNFREEZE_REASON_EXECUTING_SERVICE, + UNFREEZE_REASON_RESTRICTION_CHANGE, + UNFREEZE_REASON_COMPONENT_DISABLED, }) @Retention(RetentionPolicy.SOURCE) public @interface UnfreezeReason {} @@ -1329,7 +1382,7 @@ public final class CachedAppOptimizer { } try { - traceAppFreeze(app.processName, pid, false); + traceAppFreeze(app.processName, pid, reason); Process.setProcessFrozen(pid, app.uid, false); opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis()); @@ -1341,7 +1394,7 @@ public final class CachedAppOptimizer { } if (!opt.isFrozen()) { - Slog.d(TAG_AM, "sync unfroze " + pid + " " + app.processName); + Slog.d(TAG_AM, "sync unfroze " + pid + " " + app.processName + " for " + reason); mFreezeHandler.sendMessage( mFreezeHandler.obtainMessage(REPORT_UNFREEZE_MSG, @@ -1365,13 +1418,13 @@ public final class CachedAppOptimizer { * The caller of this function should still trigger updateOomAdj for AMS to unfreeze the app. * @param pid pid of the process to be unfrozen */ - void unfreezeProcess(int pid, @OomAdjuster.OomAdjReason int reason) { + void unfreezeProcess(int pid, @OomAdjReason int reason) { synchronized (mFreezerLock) { ProcessRecord app = mFrozenProcesses.get(pid); if (app == null) { return; } - Slog.d(TAG_AM, "quick sync unfreeze " + pid); + Slog.d(TAG_AM, "quick sync unfreeze " + pid + " for " + reason); try { freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS); } catch (RuntimeException e) { @@ -1380,7 +1433,7 @@ public final class CachedAppOptimizer { } try { - traceAppFreeze(app.processName, pid, false); + traceAppFreeze(app.processName, pid, reason); Process.setProcessFrozen(pid, app.uid, false); } catch (Exception e) { Slog.e(TAG_AM, "Unable to quick unfreeze " + pid); @@ -1388,9 +1441,15 @@ public final class CachedAppOptimizer { } } - private static void traceAppFreeze(String processName, int pid, boolean freeze) { + /** + * Trace app freeze status + * @param processName The name of the target process + * @param pid The pid of the target process + * @param reason UNFREEZE_REASON_XXX (>=0) for unfreezing and -1 for freezing + */ + private static void traceAppFreeze(String processName, int pid, int reason) { Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, ATRACE_FREEZER_TRACK, - (freeze ? "Freeze " : "Unfreeze ") + processName + ":" + pid); + (reason < 0 ? "Freeze " : "Unfreeze ") + processName + ":" + pid + " " + reason); } /** @@ -1540,12 +1599,12 @@ public final class CachedAppOptimizer { public long mOrigAnonRss; public int mProcState; public int mOomAdj; - public @OomAdjuster.OomAdjReason int mOomAdjReason; + public @OomAdjReason int mOomAdjReason; SingleCompactionStats(long[] rss, CompactSource source, String processName, long deltaAnonRss, long zramConsumed, long anonMemFreed, long origAnonRss, long cpuTimeMillis, int procState, int oomAdj, - @OomAdjuster.OomAdjReason int oomAdjReason, int uid) { + @OomAdjReason int oomAdjReason, int uid) { mRssAfterCompaction = rss; mSourceType = source; mProcessName = processName; @@ -2063,7 +2122,7 @@ public final class CachedAppOptimizer { long unfreezeTime = opt.getFreezeUnfreezeTime(); try { - traceAppFreeze(proc.processName, pid, true); + traceAppFreeze(proc.processName, pid, -1); Process.setProcessFrozen(pid, proc.uid, true); opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis()); @@ -2127,7 +2186,7 @@ public final class CachedAppOptimizer { private void reportUnfreeze(int pid, int frozenDuration, String processName, @UnfreezeReason int reason) { - EventLog.writeEvent(EventLogTags.AM_UNFREEZE, pid, processName); + EventLog.writeEvent(EventLogTags.AM_UNFREEZE, pid, processName, reason); // See above for why we're not taking mPhenotypeFlagLock here if (mRandom.nextFloat() < mFreezerStatsdSampleRate) { @@ -2201,32 +2260,52 @@ public final class CachedAppOptimizer { } } - static int getUnfreezeReasonCodeFromOomAdjReason(@OomAdjuster.OomAdjReason int oomAdjReason) { + static int getUnfreezeReasonCodeFromOomAdjReason(@OomAdjReason int oomAdjReason) { switch (oomAdjReason) { - case OomAdjuster.OOM_ADJ_REASON_ACTIVITY: + case OOM_ADJ_REASON_ACTIVITY: return UNFREEZE_REASON_ACTIVITY; - case OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER: + case OOM_ADJ_REASON_FINISH_RECEIVER: return UNFREEZE_REASON_FINISH_RECEIVER; - case OomAdjuster.OOM_ADJ_REASON_START_RECEIVER: + case OOM_ADJ_REASON_START_RECEIVER: return UNFREEZE_REASON_START_RECEIVER; - case OomAdjuster.OOM_ADJ_REASON_BIND_SERVICE: + case OOM_ADJ_REASON_BIND_SERVICE: return UNFREEZE_REASON_BIND_SERVICE; - case OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE: + case OOM_ADJ_REASON_UNBIND_SERVICE: return UNFREEZE_REASON_UNBIND_SERVICE; - case OomAdjuster.OOM_ADJ_REASON_START_SERVICE: + case OOM_ADJ_REASON_START_SERVICE: return UNFREEZE_REASON_START_SERVICE; - case OomAdjuster.OOM_ADJ_REASON_GET_PROVIDER: + case OOM_ADJ_REASON_GET_PROVIDER: return UNFREEZE_REASON_GET_PROVIDER; - case OomAdjuster.OOM_ADJ_REASON_REMOVE_PROVIDER: + case OOM_ADJ_REASON_REMOVE_PROVIDER: return UNFREEZE_REASON_REMOVE_PROVIDER; - case OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY: + case OOM_ADJ_REASON_UI_VISIBILITY: return UNFREEZE_REASON_UI_VISIBILITY; - case OomAdjuster.OOM_ADJ_REASON_ALLOWLIST: + case OOM_ADJ_REASON_ALLOWLIST: return UNFREEZE_REASON_ALLOWLIST; - case OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN: + case OOM_ADJ_REASON_PROCESS_BEGIN: return UNFREEZE_REASON_PROCESS_BEGIN; - case OomAdjuster.OOM_ADJ_REASON_PROCESS_END: + case OOM_ADJ_REASON_PROCESS_END: return UNFREEZE_REASON_PROCESS_END; + case OOM_ADJ_REASON_SHORT_FGS_TIMEOUT: + return UNFREEZE_REASON_SHORT_FGS_TIMEOUT; + case OOM_ADJ_REASON_SYSTEM_INIT: + return UNFREEZE_REASON_SYSTEM_INIT; + case OOM_ADJ_REASON_BACKUP: + return UNFREEZE_REASON_BACKUP; + case OOM_ADJ_REASON_SHELL: + return UNFREEZE_REASON_SHELL; + case OOM_ADJ_REASON_REMOVE_TASK: + return UNFREEZE_REASON_REMOVE_TASK; + case OOM_ADJ_REASON_UID_IDLE: + return UNFREEZE_REASON_UID_IDLE; + case OOM_ADJ_REASON_STOP_SERVICE: + return UNFREEZE_REASON_STOP_SERVICE; + case OOM_ADJ_REASON_EXECUTING_SERVICE: + return UNFREEZE_REASON_EXECUTING_SERVICE; + case OOM_ADJ_REASON_RESTRICTION_CHANGE: + return UNFREEZE_REASON_RESTRICTION_CHANGE; + case OOM_ADJ_REASON_COMPONENT_DISABLED: + return UNFREEZE_REASON_COMPONENT_DISABLED; default: return UNFREEZE_REASON_NONE; } diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index a1fcd424f8c1..d8cb094caa65 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -16,6 +16,8 @@ package com.android.server.am; import static android.Manifest.permission.GET_ANY_PROVIDER_TYPE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_GET_PROVIDER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_PROVIDER; import static android.content.ContentProvider.isAuthorityRedirectedForCloneProfile; import static android.os.Process.PROC_CHAR; import static android.os.Process.PROC_OUT_LONG; @@ -292,7 +294,7 @@ public class ContentProviderHelper { checkTime(startTime, "getContentProviderImpl: before updateOomAdj"); final int verifiedAdj = cpr.proc.mState.getVerifiedAdj(); boolean success = mService.updateOomAdjLocked(cpr.proc, - OomAdjuster.OOM_ADJ_REASON_GET_PROVIDER); + OOM_ADJ_REASON_GET_PROVIDER); // XXX things have changed so updateOomAdjLocked doesn't actually tell us // if the process has been successfully adjusted. So to reduce races with // it, we will check whether the process still exists. Note that this doesn't @@ -757,7 +759,7 @@ public class ContentProviderHelper { // update the app's oom adj value and each provider's usage stats if (providersPublished) { - mService.updateOomAdjLocked(r, OomAdjuster.OOM_ADJ_REASON_GET_PROVIDER); + mService.updateOomAdjLocked(r, OOM_ADJ_REASON_GET_PROVIDER); for (int i = 0, size = providers.size(); i < size; i++) { ContentProviderHolder src = providers.get(i); if (src == null || src.info == null || src.provider == null) { @@ -835,8 +837,7 @@ public class ContentProviderHelper { ContentProviderRecord localCpr = mProviderMap.getProviderByClass(comp, userId); if (localCpr.hasExternalProcessHandles()) { if (localCpr.removeExternalProcessHandleLocked(token)) { - mService.updateOomAdjLocked(localCpr.proc, - OomAdjuster.OOM_ADJ_REASON_REMOVE_PROVIDER); + mService.updateOomAdjLocked(localCpr.proc, OOM_ADJ_REASON_REMOVE_PROVIDER); } else { Slog.e(TAG, "Attempt to remove content provider " + localCpr + " with no external reference for token: " + token + "."); @@ -1506,8 +1507,7 @@ public class ContentProviderHelper { mService.stopAssociationLocked(conn.client.uid, conn.client.processName, cpr.uid, cpr.appInfo.longVersionCode, cpr.name, cpr.info.processName); if (updateOomAdj) { - mService.updateOomAdjLocked(conn.provider.proc, - OomAdjuster.OOM_ADJ_REASON_REMOVE_PROVIDER); + mService.updateOomAdjLocked(conn.provider.proc, OOM_ADJ_REASON_REMOVE_PROVIDER); } } } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index a98571b68067..365dcd9bd785 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -41,6 +41,29 @@ import static android.app.ActivityManager.PROCESS_STATE_PERSISTENT_UI; import static android.app.ActivityManager.PROCESS_STATE_SERVICE; import static android.app.ActivityManager.PROCESS_STATE_TOP; import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ALLOWLIST; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BACKUP; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BIND_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_COMPONENT_DISABLED; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_EXECUTING_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_GET_PROVIDER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_NONE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_PROVIDER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_TASK; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RESTRICTION_CHANGE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHELL; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHORT_FGS_TIMEOUT; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_STOP_SERVICE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SYSTEM_INIT; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UID_IDLE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UI_VISIBILITY; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UNBIND_SERVICE; import static android.content.Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; @@ -101,9 +124,9 @@ import static com.android.server.am.ProcessList.UNKNOWN_ADJ; import static com.android.server.am.ProcessList.VISIBLE_APP_ADJ; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH; -import android.annotation.IntDef; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.ActivityManagerInternal.OomAdjReason; import android.app.ActivityThread; import android.app.AppProtoEnums; import android.app.ApplicationExitInfo; @@ -141,8 +164,6 @@ import com.android.server.wm.ActivityServiceConnectionsHolder; import com.android.server.wm.WindowProcessController; import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -154,32 +175,6 @@ import java.util.List; public class OomAdjuster { static final String TAG = "OomAdjuster"; - static final int OOM_ADJ_REASON_NONE = 0; - static final int OOM_ADJ_REASON_ACTIVITY = 1; - static final int OOM_ADJ_REASON_FINISH_RECEIVER = 2; - static final int OOM_ADJ_REASON_START_RECEIVER = 3; - static final int OOM_ADJ_REASON_BIND_SERVICE = 4; - static final int OOM_ADJ_REASON_UNBIND_SERVICE = 5; - static final int OOM_ADJ_REASON_START_SERVICE = 6; - static final int OOM_ADJ_REASON_GET_PROVIDER = 7; - static final int OOM_ADJ_REASON_REMOVE_PROVIDER = 8; - static final int OOM_ADJ_REASON_UI_VISIBILITY = 9; - static final int OOM_ADJ_REASON_ALLOWLIST = 10; - static final int OOM_ADJ_REASON_PROCESS_BEGIN = 11; - static final int OOM_ADJ_REASON_PROCESS_END = 12; - static final int OOM_ADJ_REASON_SHORT_FGS_TIMEOUT = 13; - - @IntDef(prefix = {"OOM_ADJ_REASON_"}, - value = {OOM_ADJ_REASON_NONE, OOM_ADJ_REASON_ACTIVITY, OOM_ADJ_REASON_FINISH_RECEIVER, - OOM_ADJ_REASON_START_RECEIVER, OOM_ADJ_REASON_BIND_SERVICE, - OOM_ADJ_REASON_UNBIND_SERVICE, OOM_ADJ_REASON_START_SERVICE, - OOM_ADJ_REASON_GET_PROVIDER, OOM_ADJ_REASON_REMOVE_PROVIDER, - OOM_ADJ_REASON_UI_VISIBILITY, OOM_ADJ_REASON_ALLOWLIST, - OOM_ADJ_REASON_PROCESS_BEGIN, OOM_ADJ_REASON_PROCESS_END, - OOM_ADJ_REASON_SHORT_FGS_TIMEOUT}) - @Retention(RetentionPolicy.SOURCE) - public @interface OomAdjReason {} - public static final int oomAdjReasonToProto(@OomAdjReason int oomReason) { switch (oomReason) { case OOM_ADJ_REASON_NONE: @@ -210,6 +205,24 @@ public class OomAdjuster { return AppProtoEnums.OOM_ADJ_REASON_PROCESS_END; case OOM_ADJ_REASON_SHORT_FGS_TIMEOUT: return AppProtoEnums.OOM_ADJ_REASON_SHORT_FGS_TIMEOUT; + case OOM_ADJ_REASON_SYSTEM_INIT: + return AppProtoEnums.OOM_ADJ_REASON_SYSTEM_INIT; + case OOM_ADJ_REASON_BACKUP: + return AppProtoEnums.OOM_ADJ_REASON_BACKUP; + case OOM_ADJ_REASON_SHELL: + return AppProtoEnums.OOM_ADJ_REASON_SHELL; + case OOM_ADJ_REASON_REMOVE_TASK: + return AppProtoEnums.OOM_ADJ_REASON_REMOVE_TASK; + case OOM_ADJ_REASON_UID_IDLE: + return AppProtoEnums.OOM_ADJ_REASON_UID_IDLE; + case OOM_ADJ_REASON_STOP_SERVICE: + return AppProtoEnums.OOM_ADJ_REASON_STOP_SERVICE; + case OOM_ADJ_REASON_EXECUTING_SERVICE: + return AppProtoEnums.OOM_ADJ_REASON_EXECUTING_SERVICE; + case OOM_ADJ_REASON_RESTRICTION_CHANGE: + return AppProtoEnums.OOM_ADJ_REASON_RESTRICTION_CHANGE; + case OOM_ADJ_REASON_COMPONENT_DISABLED: + return AppProtoEnums.OOM_ADJ_REASON_COMPONENT_DISABLED; default: return AppProtoEnums.OOM_ADJ_REASON_UNKNOWN_TO_PROTO; } @@ -246,6 +259,24 @@ public class OomAdjuster { return OOM_ADJ_REASON_METHOD + "_processEnd"; case OOM_ADJ_REASON_SHORT_FGS_TIMEOUT: return OOM_ADJ_REASON_METHOD + "_shortFgs"; + case OOM_ADJ_REASON_SYSTEM_INIT: + return OOM_ADJ_REASON_METHOD + "_systemInit"; + case OOM_ADJ_REASON_BACKUP: + return OOM_ADJ_REASON_METHOD + "_backup"; + case OOM_ADJ_REASON_SHELL: + return OOM_ADJ_REASON_METHOD + "_shell"; + case OOM_ADJ_REASON_REMOVE_TASK: + return OOM_ADJ_REASON_METHOD + "_removeTask"; + case OOM_ADJ_REASON_UID_IDLE: + return OOM_ADJ_REASON_METHOD + "_uidIdle"; + case OOM_ADJ_REASON_STOP_SERVICE: + return OOM_ADJ_REASON_METHOD + "_stopService"; + case OOM_ADJ_REASON_EXECUTING_SERVICE: + return OOM_ADJ_REASON_METHOD + "_executingService"; + case OOM_ADJ_REASON_RESTRICTION_CHANGE: + return OOM_ADJ_REASON_METHOD + "_restrictionChange"; + case OOM_ADJ_REASON_COMPONENT_DISABLED: + return OOM_ADJ_REASON_METHOD + "_componentDisabled"; default: return "_unknown"; } @@ -874,8 +905,7 @@ public class OomAdjuster { } @GuardedBy("mService") - private void performUpdateOomAdjPendingTargetsLocked( - @OomAdjuster.OomAdjReason int oomAdjReason) { + private void performUpdateOomAdjPendingTargetsLocked(@OomAdjReason int oomAdjReason) { final ProcessRecord topApp = mService.getTopApp(); Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, oomAdjReasonToString(oomAdjReason)); @@ -3453,7 +3483,7 @@ public class OomAdjuster { } @GuardedBy("mService") - void unfreezeTemporarily(ProcessRecord app, @OomAdjuster.OomAdjReason int reason) { + void unfreezeTemporarily(ProcessRecord app, @OomAdjReason int reason) { if (!mCachedAppOptimizer.useFreezer()) { return; } diff --git a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java index 24cc5337b86f..f2331072ce51 100644 --- a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java +++ b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java @@ -16,6 +16,8 @@ package com.android.server.am; +import android.app.ActivityManagerInternal.OomAdjReason; + import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -51,7 +53,7 @@ final class ProcessCachedOptimizerRecord { /** * Last oom adjust change reason for this app. */ - @GuardedBy("mProcLock") private @OomAdjuster.OomAdjReason int mLastOomAdjChangeReason; + @GuardedBy("mProcLock") private @OomAdjReason int mLastOomAdjChangeReason; /** * The most recent compaction action performed for this app. @@ -139,12 +141,12 @@ final class ProcessCachedOptimizerRecord { } @GuardedBy("mProcLock") - void setLastOomAdjChangeReason(@OomAdjuster.OomAdjReason int reason) { + void setLastOomAdjChangeReason(@OomAdjReason int reason) { mLastOomAdjChangeReason = reason; } @GuardedBy("mProcLock") - @OomAdjuster.OomAdjReason + @OomAdjReason int getLastOomAdjChangeReason() { return mLastOomAdjChangeReason; } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index b1322ef510d5..a237a070c891 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -19,6 +19,8 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE; import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RESTRICTION_CHANGE; import static android.app.ActivityThread.PROC_START_SEQ_IDENT; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO; import static android.net.NetworkPolicyManager.isProcStateAllowedWhileIdleOrPowerSaveMode; @@ -2875,7 +2877,7 @@ public final class ProcessList { reasonCode, subReason, reason, !doFreeze /* async */); } killAppZygotesLocked(packageName, appId, userId, false /* force */); - mService.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_PROCESS_END); + mService.updateOomAdjLocked(OOM_ADJ_REASON_PROCESS_END); if (doFreeze) { freezePackageCgroup(packageUID, false); } @@ -5140,7 +5142,7 @@ public final class ProcessList { } }); /* Will be a no-op if nothing pending */ - mService.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_RESTRICTION_CHANGE); } } diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index afae623cd217..7aae4d5b0215 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -16,6 +16,8 @@ package com.android.server.am; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY; + import static com.android.internal.util.Preconditions.checkArgument; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -1450,7 +1452,7 @@ class ProcessRecord implements WindowProcessListener { } mService.updateLruProcessLocked(this, activityChange, null /* client */); if (updateOomAdj) { - mService.updateOomAdjLocked(this, OomAdjuster.OOM_ADJ_REASON_ACTIVITY); + mService.updateOomAdjLocked(this, OOM_ADJ_REASON_ACTIVITY); } } } diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java index 71d5d39525b4..ab71acd5f21d 100644 --- a/services/core/java/com/android/server/am/ProcessStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessStateRecord.java @@ -18,6 +18,7 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_UI_VISIBILITY; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_OOM_ADJ; import static com.android.server.am.ProcessProfileRecord.HOSTING_COMPONENT_TYPE_ACTIVITY; @@ -613,7 +614,7 @@ final class ProcessStateRecord { void forceProcessStateUpTo(int newState) { if (mRepProcState > newState) { synchronized (mProcLock) { - mRepProcState = newState; + setReportedProcState(newState); setCurProcState(newState); setCurRawProcState(newState); } @@ -766,7 +767,7 @@ final class ProcessStateRecord { Slog.i(TAG, "Setting runningRemoteAnimation=" + runningRemoteAnimation + " for pid=" + mApp.getPid()); } - mService.updateOomAdjLocked(mApp, OomAdjuster.OOM_ADJ_REASON_UI_VISIBILITY); + mService.updateOomAdjLocked(mApp, OOM_ADJ_REASON_UI_VISIBILITY); } @GuardedBy({"mService", "mProcLock"}) diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 22e2c9fd889b..8c227f5488d3 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -100,7 +100,6 @@ public class SettingsToPropertiesMapper { DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, DeviceConfig.NAMESPACE_SURFACE_FLINGER_NATIVE_BOOT, DeviceConfig.NAMESPACE_SWCODEC_NATIVE, - DeviceConfig.NAMESPACE_TETHERING, DeviceConfig.NAMESPACE_VENDOR_SYSTEM_NATIVE, DeviceConfig.NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT, DeviceConfig.NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE, diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 81ba4b813de1..a110169ac8c2 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -34,6 +34,7 @@ import static android.app.AppOpsManager.MODE_ERRORED; import static android.app.AppOpsManager.MODE_FOREGROUND; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.AppOpsManager.OP_CAMERA; +import static android.app.AppOpsManager.OP_CAMERA_SANDBOXED; import static android.app.AppOpsManager.OP_FLAGS_ALL; import static android.app.AppOpsManager.OP_FLAG_SELF; import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; @@ -42,6 +43,7 @@ import static android.app.AppOpsManager.OP_PLAY_AUDIO; import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD; +import static android.app.AppOpsManager.OP_RECORD_AUDIO_SANDBOXED; import static android.app.AppOpsManager.OP_VIBRATE; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED; @@ -3027,17 +3029,29 @@ public class AppOpsService extends IAppOpsService.Stub { packageName); } - // As a special case for OP_RECORD_AUDIO_HOTWORD, which we use only for attribution - // purposes and not as a check, also make sure that the caller is allowed to access - // the data gated by OP_RECORD_AUDIO. + // As a special case for OP_RECORD_AUDIO_HOTWORD, OP_RECEIVE_AMBIENT_TRIGGER_AUDIO and + // OP_RECORD_AUDIO_SANDBOXED which we use only for attribution purposes and not as a check, + // also make sure that the caller is allowed to access the data gated by OP_RECORD_AUDIO. // // TODO: Revert this change before Android 12. - if (code == OP_RECORD_AUDIO_HOTWORD || code == OP_RECEIVE_AMBIENT_TRIGGER_AUDIO) { - int result = checkOperation(OP_RECORD_AUDIO, uid, packageName); + int result = MODE_DEFAULT; + if (code == OP_RECORD_AUDIO_HOTWORD || code == OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + || code == OP_RECORD_AUDIO_SANDBOXED) { + result = checkOperation(OP_RECORD_AUDIO, uid, packageName); + // Check result if (result != AppOpsManager.MODE_ALLOWED) { return new SyncNotedAppOp(result, code, attributionTag, packageName); } } + // As a special case for OP_CAMERA_SANDBOXED. + if (code == OP_CAMERA_SANDBOXED) { + result = checkOperation(OP_CAMERA, uid, packageName); + // Check result + if (result != AppOpsManager.MODE_ALLOWED) { + return new SyncNotedAppOp(result, code, attributionTag, packageName); + } + } + return startOperationUnchecked(clientId, code, uid, packageName, attributionTag, Process.INVALID_UID, null, null, OP_FLAG_SELF, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, attributionFlags, diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index 7c8e6df4acdc..5127d26e6e73 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -19,6 +19,8 @@ package com.android.server.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE; +import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR; +import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR_BASE; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE; @@ -519,6 +521,9 @@ public final class AuthSession implements IBinder.DeathRecipient { try { mStatusBarService.onBiometricHelp(sensorIdToModality(sensorId), message); + final int aAcquiredInfo = acquiredInfo == FINGERPRINT_ACQUIRED_VENDOR + ? (vendorCode + FINGERPRINT_ACQUIRED_VENDOR_BASE) : acquiredInfo; + mClientReceiver.onAcquired(aAcquiredInfo, message); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 128ef0b2a802..6c26e2b0ce99 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -413,6 +413,11 @@ public class FingerprintService extends SystemService { Slog.e(TAG, "Remote exception in onAuthenticationAcquired()", e); } } + + @Override + public void onAuthenticationHelp(int acquireInfo, CharSequence helpString) { + onAuthenticationAcquired(acquireInfo); + } }; return biometricPrompt.authenticateForOperation( diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index 43e346a5bfa3..2d4066144a7f 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -323,7 +323,7 @@ public class LocaleManagerService extends SystemService { */ void notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales) { - String installingPackageName = getInstallingPackageName(appPackageName); + String installingPackageName = getInstallingPackageName(appPackageName, userId); if (installingPackageName != null) { Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, appPackageName, locales); @@ -464,7 +464,7 @@ public class LocaleManagerService extends SystemService { * Checks if the calling app is the installer of the app whose locale changed. */ private boolean isCallerInstaller(String appPackageName, int userId) { - String installingPackageName = getInstallingPackageName(appPackageName); + String installingPackageName = getInstallingPackageName(appPackageName, userId); if (installingPackageName != null) { // Get the uid of installer-on-record to compare with the calling uid. int installerUid = getPackageUid(installingPackageName, userId); @@ -513,10 +513,11 @@ public class LocaleManagerService extends SystemService { } @Nullable - String getInstallingPackageName(String packageName) { + String getInstallingPackageName(String packageName, int userId) { try { - return mContext.getPackageManager() - .getInstallSourceInfo(packageName).getInstallingPackageName(); + return mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ + 0).getPackageManager().getInstallSourceInfo( + packageName).getInstallingPackageName(); } catch (PackageManager.NameNotFoundException e) { Slog.w(TAG, "Package not found " + packageName); } diff --git a/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java b/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java index 215c653f1be7..373d3553e0eb 100644 --- a/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java +++ b/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java @@ -152,9 +152,10 @@ public class SystemAppUpdateTracker { void onPackageUpdateFinished(String packageName, int uid) { try { if ((!mUpdatedApps.contains(packageName)) && isUpdatedSystemApp(packageName)) { + int userId = UserHandle.getUserId(uid); // If a system app is updated, verify that it has an installer-on-record. String installingPackageName = mLocaleManagerService.getInstallingPackageName( - packageName); + packageName, userId); if (installingPackageName == null) { // We want to broadcast the locales info to the installer. // If this app does not have an installer then do nothing. @@ -162,7 +163,6 @@ public class SystemAppUpdateTracker { } try { - int userId = UserHandle.getUserId(uid); // Fetch the app-specific locales. // If non-empty then send the info to the installer. LocaleList appLocales = mLocaleManagerService.getApplicationLocales( diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index f0e8ede5987d..94d5aabe24e5 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -357,12 +357,16 @@ public final class MediaProjectionManagerService extends SystemService } catch (NameNotFoundException e) { throw new IllegalArgumentException("No package matching :" + packageName); } - - projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion, - ai.isPrivilegedApp()); - if (isPermanentGrant) { - mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA, - projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED); + final long callingToken = Binder.clearCallingIdentity(); + try { + projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion, + ai.isPrivilegedApp()); + if (isPermanentGrant) { + mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA, + projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED); + } + } finally { + Binder.restoreCallingIdentity(callingToken); } return projection; } @@ -418,16 +422,9 @@ public final class MediaProjectionManagerService extends SystemService if (packageName == null || packageName.isEmpty()) { throw new IllegalArgumentException("package name must not be empty"); } - MediaProjection projection; final UserHandle callingUser = Binder.getCallingUserHandle(); - final long callingToken = Binder.clearCallingIdentity(); - try { - projection = createProjectionInternal(uid, packageName, type, isPermanentGrant, - callingUser, false); - } finally { - Binder.restoreCallingIdentity(callingToken); - } - return projection; + return createProjectionInternal(uid, packageName, type, isPermanentGrant, + callingUser, false); } @Override // Binder call diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6d27fe058423..5d81dda5f5eb 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3815,6 +3815,28 @@ public class NotificationManagerService extends SystemService { } @Override + public boolean canUseFullScreenIntent(@NonNull AttributionSource attributionSource) { + final String packageName = attributionSource.getPackageName(); + final int uid = attributionSource.getUid(); + final int userId = UserHandle.getUserId(uid); + checkCallerIsSameApp(packageName, uid, userId); + + final ApplicationInfo applicationInfo; + try { + applicationInfo = mPackageManagerClient.getApplicationInfoAsUser( + packageName, PackageManager.MATCH_DIRECT_BOOT_AUTO, userId); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Failed to getApplicationInfo() in canUseFullScreenIntent()", e); + return false; + } + final boolean showStickyHunIfDenied = mFlagResolver.isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags + .SHOW_STICKY_HUN_FOR_DENIED_FSI); + return checkUseFullScreenIntentPermission(attributionSource, applicationInfo, + showStickyHunIfDenied /* isAppOpPermission */, false /* forDataDelivery */); + } + + @Override public void updateNotificationChannelGroupForPackage(String pkg, int uid, NotificationChannelGroup group) throws RemoteException { enforceSystemOrSystemUI("Caller not system or systemui"); @@ -6826,36 +6848,28 @@ public class NotificationManagerService extends SystemService { notification.flags &= ~FLAG_FSI_REQUESTED_BUT_DENIED; - if (notification.fullScreenIntent != null && ai.targetSdkVersion >= Build.VERSION_CODES.Q) { + if (notification.fullScreenIntent != null) { final boolean forceDemoteFsiToStickyHun = mFlagResolver.isEnabled( SystemUiSystemPropertiesFlags.NotificationFlags.FSI_FORCE_DEMOTE); - - final boolean showStickyHunIfDenied = mFlagResolver.isEnabled( - SystemUiSystemPropertiesFlags.NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI); - if (forceDemoteFsiToStickyHun) { makeStickyHun(notification, pkg, userId); - - } else if (showStickyHunIfDenied) { - final AttributionSource source = new AttributionSource.Builder(notificationUid) - .setPackageName(pkg) - .build(); - - final int permissionResult = mPermissionManager.checkPermissionForDataDelivery( - Manifest.permission.USE_FULL_SCREEN_INTENT, source, /* message= */ null); - - if (permissionResult != PermissionManager.PERMISSION_GRANTED) { - makeStickyHun(notification, pkg, userId); - } - } else { - int fullscreenIntentPermission = getContext().checkPermission( - android.Manifest.permission.USE_FULL_SCREEN_INTENT, -1, notificationUid); - - if (fullscreenIntentPermission != PERMISSION_GRANTED) { - notification.fullScreenIntent = null; - Slog.w(TAG, "Package " + pkg + ": Use of fullScreenIntent requires the" - + "USE_FULL_SCREEN_INTENT permission"); + final AttributionSource attributionSource = + new AttributionSource.Builder(notificationUid).setPackageName(pkg).build(); + final boolean showStickyHunIfDenied = mFlagResolver.isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags + .SHOW_STICKY_HUN_FOR_DENIED_FSI); + final boolean canUseFullScreenIntent = checkUseFullScreenIntentPermission( + attributionSource, ai, showStickyHunIfDenied /* isAppOpPermission */, + true /* forDataDelivery */); + if (!canUseFullScreenIntent) { + if (showStickyHunIfDenied) { + makeStickyHun(notification, pkg, userId); + } else { + notification.fullScreenIntent = null; + Slog.w(TAG, "Package " + pkg + ": Use of fullScreenIntent requires the" + + "USE_FULL_SCREEN_INTENT permission"); + } } } } @@ -6951,6 +6965,30 @@ public class NotificationManagerService extends SystemService { ai.packageName) == AppOpsManager.MODE_ALLOWED; } + private boolean checkUseFullScreenIntentPermission(@NonNull AttributionSource attributionSource, + @NonNull ApplicationInfo applicationInfo, boolean isAppOpPermission, + boolean forDataDelivery) { + if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.Q) { + return true; + } + if (isAppOpPermission) { + final int permissionResult; + if (forDataDelivery) { + permissionResult = mPermissionManager.checkPermissionForDataDelivery( + permission.USE_FULL_SCREEN_INTENT, attributionSource, /* message= */ null); + } else { + permissionResult = mPermissionManager.checkPermissionForPreflight( + permission.USE_FULL_SCREEN_INTENT, attributionSource); + } + return permissionResult == PermissionManager.PERMISSION_GRANTED; + } else { + final int permissionResult = getContext().checkPermission( + permission.USE_FULL_SCREEN_INTENT, attributionSource.getPid(), + attributionSource.getUid()); + return permissionResult == PERMISSION_GRANTED; + } + } + private void checkRemoteViews(String pkg, String tag, int id, Notification notification) { if (removeRemoteView(pkg, tag, id, notification.contentView)) { notification.contentView = null; diff --git a/services/core/java/com/android/server/notification/ZenLog.java b/services/core/java/com/android/server/notification/ZenLog.java index 35b94e7ccd63..88d23ce3f9a1 100644 --- a/services/core/java/com/android/server/notification/ZenLog.java +++ b/services/core/java/com/android/server/notification/ZenLog.java @@ -27,6 +27,7 @@ import android.service.notification.Condition; import android.service.notification.IConditionProvider; import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeDiff; import android.util.Log; import android.util.Slog; @@ -146,13 +147,13 @@ public class ZenLog { public static void traceConfig(String reason, ZenModeConfig oldConfig, ZenModeConfig newConfig) { - ZenModeConfig.Diff diff = ZenModeConfig.diff(oldConfig, newConfig); - if (diff.isEmpty()) { + ZenModeDiff.ConfigDiff diff = new ZenModeDiff.ConfigDiff(oldConfig, newConfig); + if (diff == null || !diff.hasDiff()) { append(TYPE_CONFIG, reason + " no changes"); } else { append(TYPE_CONFIG, reason + ",\n" + (newConfig != null ? newConfig.toString() : null) - + ",\n" + ZenModeConfig.diff(oldConfig, newConfig)); + + ",\n" + diff); } } diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java index ad77ef7ca975..9127a93a46ee 100644 --- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java +++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java @@ -187,6 +187,9 @@ public final class SuspendPackageHelper { if (changed) { changedPackagesList.add(packageName); changedUids.add(UserHandle.getUid(userId, packageState.getAppId())); + } else { + Slog.w(TAG, "No change is needed for package: " + packageName + + ". Skipping suspending/un-suspending."); } } diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java index 401eac6e2d19..7a5664f8135e 100644 --- a/services/core/java/com/android/server/policy/AppOpsPolicy.java +++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java @@ -316,6 +316,7 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat private int resolveDatasourceOp(int code, int uid, @NonNull String packageName, @Nullable String attributionTag) { code = resolveRecordAudioOp(code, uid); + code = resolveSandboxedServiceOp(code, uid); if (attributionTag == null) { return code; } @@ -439,6 +440,28 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat return code; } + private int resolveSandboxedServiceOp(int code, int uid) { + if (!Process.isIsolated(uid) // simple check which fails-fast for the common case + || !(code == AppOpsManager.OP_RECORD_AUDIO || code == AppOpsManager.OP_CAMERA)) { + return code; + } + final HotwordDetectionServiceIdentity hotwordDetectionServiceIdentity = + mVoiceInteractionManagerInternal.getHotwordDetectionServiceIdentity(); + if (hotwordDetectionServiceIdentity != null + && uid == hotwordDetectionServiceIdentity.getIsolatedUid()) { + // Upgrade the op such that no indicators is shown for camera or audio service. This + // will bypass the permission checking for the original OP_RECORD_AUDIO and OP_CAMERA. + switch (code) { + case AppOpsManager.OP_RECORD_AUDIO: + return AppOpsManager.OP_RECORD_AUDIO_SANDBOXED; + case AppOpsManager.OP_CAMERA: + return AppOpsManager.OP_CAMERA_SANDBOXED; + } + } + return code; + } + + private int resolveUid(int code, int uid) { // The HotwordDetectionService is an isolated service, which ordinarily cannot hold // permissions. So we allow it to assume the owning package identity for certain diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index b1b0c559aad4..4a03628ab8e5 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -3569,6 +3569,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub private void dumpWallpaper(WallpaperData wallpaper, PrintWriter pw) { if (wallpaper == null) { pw.println(" (null entry)"); + return; } pw.print(" User "); pw.print(wallpaper.userId); pw.print(": id="); pw.print(wallpaper.wallpaperId); diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java index 83804f75afea..32f7b9682fb7 100644 --- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java +++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java @@ -466,8 +466,7 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, } boolean isAnimatingByRecents(@NonNull Task task) { - return task.isAnimatingByRecents() - || mService.mAtmService.getTransitionController().inRecentsTransition(task); + return task.isAnimatingByRecents(); } void dump(PrintWriter pw, String prefix) { diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index fa3a186a6153..f3001133338a 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -25,6 +25,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFI import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY; +import static com.android.internal.util.DumpUtils.dumpSparseArray; +import static com.android.internal.util.DumpUtils.dumpSparseArrayValues; import static com.android.server.accessibility.AccessibilityTraceFileProto.ENTRY; import static com.android.server.accessibility.AccessibilityTraceFileProto.MAGIC_NUMBER; import static com.android.server.accessibility.AccessibilityTraceFileProto.MAGIC_NUMBER_H; @@ -542,15 +544,12 @@ final class AccessibilityController { } void dump(PrintWriter pw, String prefix) { - for (int i = 0; i < mDisplayMagnifiers.size(); i++) { - final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.valueAt(i); - if (displayMagnifier != null) { - displayMagnifier.dump(pw, prefix - + "Magnification display# " + mDisplayMagnifiers.keyAt(i)); - } - } - pw.println(prefix - + "mWindowsForAccessibilityObserver=" + mWindowsForAccessibilityObserver); + dumpSparseArray(pw, prefix, mDisplayMagnifiers, "magnification display", + (index, key) -> pw.printf("%sDisplay #%d:", prefix + " ", key), + dm -> dm.dump(pw, "")); + dumpSparseArrayValues(pw, prefix, mWindowsForAccessibilityObserver, + "windows for accessibility observer"); + mAccessibilityWindowsPopulator.dump(pw, prefix); } void onFocusChanged(InputTarget lastTarget, InputTarget newTarget) { diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java index 21b241a0d117..70f20075b48c 100644 --- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java +++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java @@ -16,6 +16,9 @@ package com.android.server.wm; +import static com.android.internal.util.DumpUtils.KeyDumper; +import static com.android.internal.util.DumpUtils.ValueDumper; +import static com.android.internal.util.DumpUtils.dumpSparseArray; import static com.android.server.wm.utils.RegionUtils.forEachRect; import android.annotation.NonNull; @@ -40,6 +43,7 @@ import android.window.WindowInfosListener; import com.android.internal.annotations.GuardedBy; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -562,6 +566,35 @@ public final class AccessibilityWindowsPopulator extends WindowInfosListener { notifyWindowsChanged(displayIdsForWindowsChanged); } + void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.println("AccessibilityWindowsPopulator"); + String prefix2 = prefix + " "; + + pw.print(prefix2); pw.print("mWindowsNotificationEnabled: "); + pw.println(mWindowsNotificationEnabled); + + if (mVisibleWindows.isEmpty()) { + pw.print(prefix2); pw.println("No visible windows"); + } else { + pw.print(prefix2); pw.print(mVisibleWindows.size()); + pw.print(" visible windows: "); pw.println(mVisibleWindows); + } + KeyDumper noKeyDumper = (i, k) -> {}; // display id is already shown on value; + KeyDumper displayDumper = (i, d) -> pw.printf("%sDisplay #%d: ", prefix, d); + // Ideally magnificationSpecDumper should use spec.dump(pw), but there is no such method + ValueDumper<MagnificationSpec> magnificationSpecDumper = spec -> pw.print(spec); + + dumpSparseArray(pw, prefix2, mDisplayInfos, "display info", noKeyDumper, d -> pw.print(d)); + dumpSparseArray(pw, prefix2, mInputWindowHandlesOnDisplays, "window handles on display", + displayDumper, list -> pw.print(list)); + dumpSparseArray(pw, prefix2, mMagnificationSpecInverseMatrix, "magnification spec matrix", + noKeyDumper, matrix -> matrix.dump(pw)); + dumpSparseArray(pw, prefix2, mCurrentMagnificationSpec, "current magnification spec", + noKeyDumper, magnificationSpecDumper); + dumpSparseArray(pw, prefix2, mPreviousMagnificationSpec, "previous magnification spec", + noKeyDumper, magnificationSpecDumper); + } + @GuardedBy("mLock") private void releaseResources() { mInputWindowHandlesOnDisplays.clear(); diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index ff1c28ad1973..62144401223b 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -78,8 +78,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManagerInternal; -import android.content.pm.ParceledListSlice; -import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.os.Binder; import android.os.Bundle; @@ -1645,18 +1643,15 @@ class ActivityClientController extends IActivityClientController.Stub { launchedFromHome = root.isLaunchSourceType(ActivityRecord.LAUNCH_SOURCE_TYPE_HOME); } - // If the activity is one of the main entry points for the application, then we should + // If the activity was launched directly from the home screen, then we should // refrain from finishing the activity and instead move it to the back to keep it in // memory. The requirements for this are: // 1. The activity is the last running activity in the task. // 2. The current activity is the base activity for the task. - // 3. a. If the activity was launched by the home process, we trust that its intent - // was resolved, so we check if the it is a main intent for the application. - // b. Otherwise, we query Package Manager to verify whether the activity is a - // launcher activity for the application. + // 3. The activity was launched by the home process, and is one of the main entry + // points for the application. if (baseActivityIntent != null && isLastRunningActivity - && ((launchedFromHome && ActivityRecord.isMainIntent(baseActivityIntent)) - || isLauncherActivity(baseActivityIntent.getComponent()))) { + && launchedFromHome && ActivityRecord.isMainIntent(baseActivityIntent)) { moveActivityTaskToBack(token, true /* nonRoot */); return; } @@ -1668,31 +1663,6 @@ class ActivityClientController extends IActivityClientController.Stub { } } - /** - * Queries PackageManager to see if the given activity is one of the main entry point for the - * application. This should not be called with the WM lock held. - */ - @SuppressWarnings("unchecked") - private boolean isLauncherActivity(@NonNull ComponentName activity) { - final Intent queryIntent = new Intent(Intent.ACTION_MAIN); - queryIntent.addCategory(Intent.CATEGORY_LAUNCHER); - queryIntent.setPackage(activity.getPackageName()); - try { - final ParceledListSlice<ResolveInfo> resolved = - mService.getPackageManager().queryIntentActivities( - queryIntent, null, 0, mContext.getUserId()); - if (resolved == null) return false; - for (final ResolveInfo ri : resolved.getList()) { - if (ri.getComponentInfo().getComponentName().equals(activity)) { - return true; - } - } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to query intent activities", e); - } - return false; - } - @Override public void enableTaskLocaleOverride(IBinder token) { if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index fb1f8994dbc5..1bcc05e6f116 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3544,8 +3544,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // the best capture timing (e.g. IME window capture), // No need additional task capture while task is controlled by RecentsAnimation. if (mAtmService.mWindowManager.mTaskSnapshotController != null - && !(task.isAnimatingByRecents() - || mTransitionController.inRecentsTransition(task))) { + && !task.isAnimatingByRecents()) { final ArraySet<Task> tasks = Sets.newArraySet(task); mAtmService.mWindowManager.mTaskSnapshotController.snapshotTasks(tasks); mAtmService.mWindowManager.mTaskSnapshotController @@ -10535,11 +10534,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override boolean isSyncFinished() { - if (task != null && mTransitionController.isTransientHide(task)) { - // The activity keeps visibleRequested but may be hidden later, so no need to wait for - // it to be drawn. - return true; - } if (!super.isSyncFinished()) return false; if (mDisplayContent != null && mDisplayContent.mUnknownAppVisibilityController .isVisibilityUnknown(this)) { diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 7c1e9071b926..bfe298653584 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -577,7 +577,6 @@ public class ActivityStartController { final Transition transition = controller.getCollectingTransition(); if (transition != null) { transition.setRemoteAnimationApp(r.app.getThread()); - controller.collect(task); controller.setTransientLaunch(r, TaskDisplayArea.getRootTaskAbove(rootTask)); } task.moveToFront("startExistingRecents"); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 38f13ec15987..c5e75faf2c6c 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2548,6 +2548,7 @@ class ActivityStarter { mAvoidMoveToFront = false; mFrozeTaskList = false; mTransientLaunch = false; + mPriorAboveTask = null; mDisplayLockAndOccluded = false; mVoiceSession = null; diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 12fe6a0dba25..8123c07052b0 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -831,7 +831,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { private final Runnable mUpdateOomAdjRunnable = new Runnable() { @Override public void run() { - mAmInternal.updateOomAdj(); + mAmInternal.updateOomAdj(ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY); } }; diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index a49ecce8324c..d916a1be1a03 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -23,6 +23,7 @@ import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.Handler; import android.os.Trace; import android.util.ArraySet; import android.util.Slog; @@ -172,7 +173,7 @@ class BLASTSyncEngine { if (ran) { return; } - mWm.mH.removeCallbacks(this); + mHandler.removeCallbacks(this); ran = true; SurfaceControl.Transaction t = new SurfaceControl.Transaction(); for (WindowContainer wc : wcAwaitingCommit) { @@ -199,13 +200,13 @@ class BLASTSyncEngine { }; CommitCallback callback = new CommitCallback(); merged.addTransactionCommittedListener((r) -> { r.run(); }, callback::onCommitted); - mWm.mH.postDelayed(callback, BLAST_TIMEOUT_DURATION); + mHandler.postDelayed(callback, BLAST_TIMEOUT_DURATION); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionReady"); mListener.onTransactionReady(mSyncId, merged); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); mActiveSyncs.remove(mSyncId); - mWm.mH.removeCallbacks(mOnTimeout); + mHandler.removeCallbacks(mOnTimeout); // Immediately start the next pending sync-transaction if there is one. if (mActiveSyncs.size() == 0 && !mPendingSyncSets.isEmpty()) { @@ -216,7 +217,7 @@ class BLASTSyncEngine { throw new IllegalStateException("Pending Sync Set didn't start a sync."); } // Post this so that the now-playing transition setup isn't interrupted. - mWm.mH.post(() -> { + mHandler.post(() -> { synchronized (mWm.mGlobalLock) { pt.mApplySync.run(); } @@ -269,6 +270,7 @@ class BLASTSyncEngine { } private final WindowManagerService mWm; + private final Handler mHandler; private int mNextSyncId = 0; private final SparseArray<SyncGroup> mActiveSyncs = new SparseArray<>(); @@ -280,7 +282,13 @@ class BLASTSyncEngine { private final ArrayList<PendingSyncSet> mPendingSyncSets = new ArrayList<>(); BLASTSyncEngine(WindowManagerService wms) { + this(wms, wms.mH); + } + + @VisibleForTesting + BLASTSyncEngine(WindowManagerService wms, Handler mainHandler) { mWm = wms; + mHandler = mainHandler; } /** @@ -325,7 +333,7 @@ class BLASTSyncEngine { @VisibleForTesting void scheduleTimeout(SyncGroup s, long timeoutMs) { - mWm.mH.postDelayed(s.mOnTimeout, timeoutMs); + mHandler.postDelayed(s.mOnTimeout, timeoutMs); } void addToSyncSet(int id, WindowContainer wc) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 73f4b5beea50..fb592e124920 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3006,7 +3006,8 @@ class Task extends TaskFragment { /** Checking if self or its child tasks are animated by recents animation. */ boolean isAnimatingByRecents() { - return isAnimating(CHILDREN, ANIMATION_TYPE_RECENTS); + return isAnimating(CHILDREN, ANIMATION_TYPE_RECENTS) + || mTransitionController.isTransientHide(this); } WindowState getTopVisibleAppMainWindow() { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index e209ef97fd7b..3a909cedc5ab 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -109,7 +109,7 @@ import java.util.function.Predicate; */ class Transition implements BLASTSyncEngine.TransactionReadyListener { private static final String TAG = "Transition"; - private static final String TRACE_NAME_PLAY_TRANSITION = "PlayTransition"; + private static final String TRACE_NAME_PLAY_TRANSITION = "playing"; /** The default package for resources */ private static final String DEFAULT_PACKAGE = "android"; @@ -511,8 +511,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (mParticipants.contains(wc)) return; // Wallpaper is like in a static drawn state unless display may have changes, so exclude // the case to reduce transition latency waiting for the unchanged wallpaper to redraw. - final boolean needSyncDraw = !isWallpaper(wc) || mParticipants.contains(wc.mDisplayContent); - if (needSyncDraw) { + final boolean needSync = (!isWallpaper(wc) || mParticipants.contains(wc.mDisplayContent)) + // Transient-hide may be hidden later, so no need to request redraw. + && !isInTransientHide(wc); + if (needSync) { mSyncEngine.addToSyncSet(mSyncId, wc); } ChangeInfo info = mChanges.get(wc); @@ -881,8 +883,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ void finishTransition() { if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER) && mIsPlayerEnabled) { - Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, TRACE_NAME_PLAY_TRANSITION, - System.identityHashCode(this)); + asyncTraceEnd(System.identityHashCode(this)); } mLogger.mFinishTimeNs = SystemClock.elapsedRealtimeNanos(); mController.mLoggerHandler.post(mLogger::logOnFinish); @@ -918,6 +919,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final WindowContainer<?> participant = mParticipants.valueAt(i); final ActivityRecord ar = participant.asActivityRecord(); if (ar != null) { + final Task task = ar.getTask(); + if (task == null) continue; boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(ar); // We need both the expected visibility AND current requested-visibility to be // false. If it is expected-visible but not currently visible, it means that @@ -936,9 +939,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (commitVisibility) { ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Commit activity becoming invisible: %s", ar); - final Task task = ar.getTask(); - if (task != null && !task.isVisibleRequested() - && mTransientLaunches != null) { + if (mTransientLaunches != null && !task.isVisibleRequested()) { // If transition is transient, then snapshots are taken at end of // transition. mController.mSnapshotController.mTaskSnapshotController @@ -963,8 +964,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Since transient launches don't automatically take focus, make sure we // synchronize focus since we committed to the launch. - if (ar.isTopRunningActivity()) { - ar.moveFocusableActivityToTop("transitionFinished"); + if (!task.isFocused() && ar.isTopRunningActivity()) { + mController.mAtm.setLastResumedActivityUncheckLocked(ar, + "transitionFinished"); } } continue; @@ -1332,8 +1334,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.getTransitionPlayer().onTransitionReady( mToken, info, transaction, mFinishTransaction); if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) { - Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, TRACE_NAME_PLAY_TRANSITION, - System.identityHashCode(this)); + asyncTraceBegin(TRACE_NAME_PLAY_TRANSITION, System.identityHashCode(this)); } } catch (RemoteException e) { // If there's an exception when trying to send the mergedTransaction to the @@ -2320,6 +2321,14 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return isCollecting() && mSyncId >= 0; } + static void asyncTraceBegin(@NonNull String name, int cookie) { + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_WINDOW_MANAGER, TAG, name, cookie); + } + + static void asyncTraceEnd(int cookie) { + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_WINDOW_MANAGER, TAG, cookie); + } + @VisibleForTesting static class ChangeInfo { private static final int FLAG_NONE = 0; diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 8d5660701994..3bf896939f49 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -37,7 +37,6 @@ import android.os.IRemoteCallback; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; -import android.os.Trace; import android.util.ArrayMap; import android.util.Slog; import android.util.TimeUtils; @@ -334,28 +333,6 @@ class TransitionController { return inCollectingTransition(wc) || inPlayingTransition(wc); } - boolean inRecentsTransition(@NonNull WindowContainer wc) { - for (WindowContainer p = wc; p != null; p = p.getParent()) { - // TODO(b/221417431): replace this with deterministic snapshots - if (mCollectingTransition == null) break; - if ((mCollectingTransition.getFlags() & TRANSIT_FLAG_IS_RECENTS) != 0 - && mCollectingTransition.mParticipants.contains(wc)) { - return true; - } - } - - for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { - for (WindowContainer p = wc; p != null; p = p.getParent()) { - // TODO(b/221417431): replace this with deterministic snapshots - if ((mPlayingTransitions.get(i).getFlags() & TRANSIT_FLAG_IS_RECENTS) != 0 - && mPlayingTransitions.get(i).mParticipants.contains(p)) { - return true; - } - } - } - return false; - } - /** @return {@code true} if wc is in a participant subtree */ boolean isTransitionOnDisplay(@NonNull DisplayContent dc) { if (mCollectingTransition != null && mCollectingTransition.isOnDisplay(dc)) { @@ -773,12 +750,12 @@ class TransitionController { // happening in app), so pause task snapshot persisting to not increase the load. mAtm.mWindowManager.mSnapshotController.setPause(true); mAnimatingState = true; - Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "transitAnim", 0); + Transition.asyncTraceBegin("animating", 0x41bfaf1 /* hashcode of TAG */); } else if (!animatingState && mAnimatingState) { t.setEarlyWakeupEnd(); mAtm.mWindowManager.mSnapshotController.setPause(false); mAnimatingState = false; - Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER, "transitAnim", 0); + Transition.asyncTraceEnd(0x41bfaf1 /* hashcode of TAG */); } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index cd4d6e4f1600..8fecf111d98c 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -6697,9 +6697,8 @@ public class WindowManagerService extends IWindowManager.Stub mInputManagerCallback.dump(pw, " "); mSnapshotController.dump(pw, " "); - if (mAccessibilityController.hasCallbacks()) { - mAccessibilityController.dump(pw, " "); - } + + dumpAccessibilityController(pw, /* force= */ false); if (dumpAll) { final WindowState imeWindow = mRoot.getCurrentInputMethodWindow(); @@ -6736,6 +6735,23 @@ public class WindowManagerService extends IWindowManager.Stub } } + private void dumpAccessibilityController(PrintWriter pw, boolean force) { + boolean hasCallbacks = mAccessibilityController.hasCallbacks(); + if (!hasCallbacks && !force) { + return; + } + if (!hasCallbacks) { + pw.println("AccessibilityController doesn't have callbacks, but printing it anways:"); + } else { + pw.println("AccessibilityController:"); + } + mAccessibilityController.dump(pw, " "); + } + + private void dumpAccessibilityLocked(PrintWriter pw) { + dumpAccessibilityController(pw, /* force= */ true); + } + private boolean dumpWindows(PrintWriter pw, String name, boolean dumpAll) { final ArrayList<WindowState> windows = new ArrayList(); if ("apps".equals(name) || "visible".equals(name) || "visible-apps".equals(name)) { @@ -6855,6 +6871,7 @@ public class WindowManagerService extends IWindowManager.Stub pw.println(" d[isplays]: active display contents"); pw.println(" t[okens]: token list"); pw.println(" w[indows]: window list"); + pw.println(" a11y[accessibility]: accessibility-related state"); pw.println(" package-config: installed packages having app-specific config"); pw.println(" trace: print trace status and write Winscope trace to file"); pw.println(" cmd may also be a NAME to dump windows. NAME may"); @@ -6918,6 +6935,11 @@ public class WindowManagerService extends IWindowManager.Stub dumpWindowsLocked(pw, true, null); } return; + } else if ("accessibility".equals(cmd) || "a11y".equals(cmd)) { + synchronized (mGlobalLock) { + dumpAccessibilityLocked(pw); + } + return; } else if ("all".equals(cmd)) { synchronized (mGlobalLock) { dumpWindowsLocked(pw, true, null); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 6e3924baadf3..e5a49c3a0ee2 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5647,7 +5647,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override boolean isSyncFinished() { - if (!isVisibleRequested()) { + if (!isVisibleRequested() || isFullyTransparent()) { // Don't wait for invisible windows. However, we don't alter the state in case the // window becomes visible while the sync group is still active. return true; diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index aeb4801628f2..208a4be9a70e 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -161,7 +161,8 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, @Override public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { - Slog.d(TAG, "in onStatusChanged with status: " + status + ", and source: " + source); + Slog.d(TAG, "in onStatusChanged for: " + componentName + ", with status: " + + status + ", and source: " + source); // Auth entry was selected, and it did not have any underlying credentials if (status == ProviderSession.Status.NO_CREDENTIALS_FROM_AUTH_ENTRY) { diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java index 0c3d3f4ed6d2..9ec0ecd93b3c 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java @@ -26,7 +26,6 @@ import android.credentials.ui.ProviderPendingIntentResponse; import android.os.ICancellationSignal; import android.service.credentials.CallingAppInfo; import android.service.credentials.ClearCredentialStateRequest; -import android.util.Log; import android.util.Slog; /** @@ -81,7 +80,7 @@ public final class ProviderClearSession extends ProviderSession<ClearCredentialS @Override public void onProviderResponseSuccess(@Nullable Void response) { - Log.i(TAG, "in onProviderResponseSuccess"); + Slog.d(TAG, "Remote provider responded with a valid response: " + mComponentName); mProviderResponseSet = true; updateStatusAndInvokeCallback(Status.COMPLETE, /*source=*/ CredentialsSource.REMOTE_PROVIDER); @@ -105,7 +104,7 @@ public final class ProviderClearSession extends ProviderSession<ClearCredentialS updateStatusAndInvokeCallback(Status.SERVICE_DEAD, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } else { - Slog.i(TAG, "Component names different in onProviderServiceDied - " + Slog.w(TAG, "Component names different in onProviderServiceDied - " + "this should not happen"); } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 8b9255a9c56b..09433dbb0c52 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -37,7 +37,6 @@ import android.service.credentials.CreateCredentialRequest; import android.service.credentials.CreateEntry; import android.service.credentials.CredentialProviderService; import android.service.credentials.RemoteEntry; -import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -93,7 +92,8 @@ public final class ProviderCreateSession extends ProviderSession< createRequestSession.mHybridService ); } - Log.i(TAG, "Unable to create provider session"); + Slog.d(TAG, "Unable to create provider session for: " + + providerInfo.getComponentName()); return null; } @@ -122,7 +122,6 @@ public final class ProviderCreateSession extends ProviderSession< return new CreateCredentialRequest(callingAppInfo, capability, clientRequest.getCredentialData()); } - Log.i(TAG, "Unable to create provider request - capabilities do not match"); return null; } @@ -146,7 +145,7 @@ public final class ProviderCreateSession extends ProviderSession< @Override public void onProviderResponseSuccess( @Nullable BeginCreateCredentialResponse response) { - Log.i(TAG, "in onProviderResponseSuccess"); + Slog.d(TAG, "Remote provider responded with a valid response: " + mComponentName); onSetInitialRemoteResponse(response); } @@ -169,7 +168,7 @@ public final class ProviderCreateSession extends ProviderSession< updateStatusAndInvokeCallback(Status.SERVICE_DEAD, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } else { - Slog.i(TAG, "Component names different in onProviderServiceDied - " + Slog.w(TAG, "Component names different in onProviderServiceDied - " + "this should not happen"); } } @@ -180,7 +179,6 @@ public final class ProviderCreateSession extends ProviderSession< } private void onSetInitialRemoteResponse(BeginCreateCredentialResponse response) { - Log.i(TAG, "onSetInitialRemoteResponse with save entries"); mProviderResponse = response; mProviderResponseDataHandler.addResponseContent(response.getCreateEntries(), response.getRemoteCreateEntry()); @@ -199,14 +197,12 @@ public final class ProviderCreateSession extends ProviderSession< @Nullable protected CreateCredentialProviderData prepareUiData() throws IllegalArgumentException { - Log.i(TAG, "In prepareUiData"); if (!ProviderSession.isUiInvokingStatus(getStatus())) { - Log.i(TAG, "In prepareUiData not in uiInvokingStatus"); + Slog.d(TAG, "No data for UI from: " + mComponentName.flattenToString()); return null; } if (mProviderResponse != null && !mProviderResponseDataHandler.isEmptyResponse()) { - Log.i(TAG, "In prepareUiData save entries not null"); return mProviderResponseDataHandler.toCreateCredentialProviderData(); } return null; @@ -218,7 +214,7 @@ public final class ProviderCreateSession extends ProviderSession< switch (entryType) { case SAVE_ENTRY_KEY: if (mProviderResponseDataHandler.getCreateEntry(entryKey) == null) { - Log.i(TAG, "Unexpected save entry key"); + Slog.w(TAG, "Unexpected save entry key"); invokeCallbackOnInternalInvalidState(); return; } @@ -226,14 +222,14 @@ public final class ProviderCreateSession extends ProviderSession< break; case REMOTE_ENTRY_KEY: if (mProviderResponseDataHandler.getRemoteEntry(entryKey) == null) { - Log.i(TAG, "Unexpected remote entry key"); + Slog.w(TAG, "Unexpected remote entry key"); invokeCallbackOnInternalInvalidState(); return; } onRemoteEntrySelected(providerPendingIntentResponse); break; default: - Log.i(TAG, "Unsupported entry type selected"); + Slog.w(TAG, "Unsupported entry type selected"); invokeCallbackOnInternalInvalidState(); } } @@ -268,7 +264,7 @@ public final class ProviderCreateSession extends ProviderSession< if (credentialResponse != null) { mCallbacks.onFinalResponseReceived(mComponentName, credentialResponse); } else { - Log.i(TAG, "onSaveEntrySelected - no response or error found in pending " + Slog.w(TAG, "onSaveEntrySelected - no response or error found in pending " + "intent response"); invokeCallbackOnInternalInvalidState(); } @@ -284,14 +280,14 @@ public final class ProviderCreateSession extends ProviderSession< private CreateCredentialException maybeGetPendingIntentException( ProviderPendingIntentResponse pendingIntentResponse) { if (pendingIntentResponse == null) { - Log.i(TAG, "pendingIntentResponse is null"); + Slog.w(TAG, "pendingIntentResponse is null"); return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREATE_OPTIONS); } if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { CreateCredentialException exception = PendingIntentResultHandler .extractCreateCredentialException(pendingIntentResponse.getResultData()); if (exception != null) { - Log.i(TAG, "Pending intent contains provider exception"); + Slog.d(TAG, "Pending intent contains provider exception"); return exception; } } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) { @@ -343,7 +339,7 @@ public final class ProviderCreateSession extends ProviderSession< public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) { if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) { - Log.i(TAG, "Remote entry being dropped as it does not meet the restriction" + Slog.w(TAG, "Remote entry being dropped as it does not meet the restriction" + "checks."); return; } diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 8d3d06469944..0c2b5633d501 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -115,7 +115,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential getRequestSession.mHybridService ); } - Log.i(TAG, "Unable to create provider session"); + Slog.d(TAG, "Unable to create provider session for: " + + providerInfo.getComponentName()); return null; } @@ -146,17 +147,15 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential android.credentials.GetCredentialRequest clientRequest, CredentialProviderInfo info ) { + Slog.d(TAG, "Filtering request options for: " + info.getComponentName()); List<CredentialOption> filteredOptions = new ArrayList<>(); for (CredentialOption option : clientRequest.getCredentialOptions()) { if (providerCapabilities.contains(option.getType()) && isProviderAllowed(option, info.getComponentName()) && checkSystemProviderRequirement(option, info.isSystemProvider())) { - Log.i(TAG, "In createProviderRequest - capability found : " - + option.getType()); + Slog.d(TAG, "Option of type: " + option.getType() + " meets all filtering" + + "conditions"); filteredOptions.add(option); - } else { - Log.i(TAG, "In createProviderRequest - capability not " - + "found, or provider not allowed : " + option.getType()); } } if (!filteredOptions.isEmpty()) { @@ -165,15 +164,14 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential .setCredentialOptions( filteredOptions).build(); } - Log.i(TAG, "In createProviderRequest - returning null"); + Slog.d(TAG, "No options filtered"); return null; } private static boolean isProviderAllowed(CredentialOption option, ComponentName componentName) { if (!option.getAllowedProviders().isEmpty() && !option.getAllowedProviders().contains( componentName)) { - Log.d(TAG, "Provider allow list specified but does not contain this provider: " - + componentName.flattenToString()); + Slog.d(TAG, "Provider allow list specified but does not contain this provider"); return false; } return true; @@ -182,7 +180,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential private static boolean checkSystemProviderRequirement(CredentialOption option, boolean isSystemProvider) { if (option.isSystemProviderRequired() && !isSystemProvider) { - Log.d(TAG, "System provider required, but this service is not a system provider"); + Slog.d(TAG, "System provider required, but this service is not a system provider"); return false; } return true; @@ -210,6 +208,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** Called when the provider response has been updated by an external source. */ @Override // Callback from the remote provider public void onProviderResponseSuccess(@Nullable BeginGetCredentialResponse response) { + Slog.d(TAG, "Remote provider responded with a valid response: " + mComponentName); onSetInitialRemoteResponse(response); } @@ -231,7 +230,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential updateStatusAndInvokeCallback(Status.SERVICE_DEAD, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } else { - Slog.i(TAG, "Component names different in onProviderServiceDied - " + Slog.w(TAG, "Component names different in onProviderServiceDied - " + "this should not happen"); } } @@ -244,13 +243,14 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential @Override // Selection call from the request provider protected void onUiEntrySelected(String entryType, String entryKey, ProviderPendingIntentResponse providerPendingIntentResponse) { - Log.i(TAG, "onUiEntrySelected with entryKey: " + entryKey); + Slog.d(TAG, "onUiEntrySelected with entryType: " + entryType + ", and entryKey: " + + entryKey); switch (entryType) { case CREDENTIAL_ENTRY_KEY: CredentialEntry credentialEntry = mProviderResponseDataHandler .getCredentialEntry(entryKey); if (credentialEntry == null) { - Log.i(TAG, "Unexpected credential entry key"); + Slog.w(TAG, "Unexpected credential entry key"); invokeCallbackOnInternalInvalidState(); return; } @@ -259,7 +259,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential case ACTION_ENTRY_KEY: Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey); if (actionEntry == null) { - Log.i(TAG, "Unexpected action entry key"); + Slog.w(TAG, "Unexpected action entry key"); invokeCallbackOnInternalInvalidState(); return; } @@ -269,21 +269,21 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential Action authenticationEntry = mProviderResponseDataHandler .getAuthenticationAction(entryKey); if (authenticationEntry == null) { - Log.i(TAG, "Unexpected authenticationEntry key"); + Slog.w(TAG, "Unexpected authenticationEntry key"); invokeCallbackOnInternalInvalidState(); return; } boolean additionalContentReceived = onAuthenticationEntrySelected(providerPendingIntentResponse); if (additionalContentReceived) { - Log.i(TAG, "Additional content received - removing authentication entry"); + Slog.d(TAG, "Additional content received - removing authentication entry"); mProviderResponseDataHandler.removeAuthenticationAction(entryKey); if (!mProviderResponseDataHandler.isEmptyResponse()) { updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED, /*source=*/ CredentialsSource.AUTH_ENTRY); } } else { - Log.i(TAG, "Additional content not received"); + Slog.d(TAG, "Additional content not received from authentication entry"); mProviderResponseDataHandler .updateAuthEntryWithNoCredentialsReceived(entryKey); updateStatusAndInvokeCallback(Status.NO_CREDENTIALS_FROM_AUTH_ENTRY, @@ -294,12 +294,12 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential if (mProviderResponseDataHandler.getRemoteEntry(entryKey) != null) { onRemoteEntrySelected(providerPendingIntentResponse); } else { - Log.i(TAG, "Unexpected remote entry key"); + Slog.d(TAG, "Unexpected remote entry key"); invokeCallbackOnInternalInvalidState(); } break; default: - Log.i(TAG, "Unsupported entry type selected"); + Slog.w(TAG, "Unsupported entry type selected"); invokeCallbackOnInternalInvalidState(); } } @@ -320,26 +320,24 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential @Override // Call from request session to data to be shown on the UI @Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException { - Log.i(TAG, "In prepareUiData"); if (!ProviderSession.isUiInvokingStatus(getStatus())) { - Log.i(TAG, "In prepareUiData - provider does not want to show UI: " - + mComponentName.flattenToString()); + Slog.d(TAG, "No data for UI from: " + mComponentName.flattenToString()); return null; } if (mProviderResponse != null && !mProviderResponseDataHandler.isEmptyResponse()) { return mProviderResponseDataHandler.toGetCredentialProviderData(); } - Log.i(TAG, "In prepareUiData response null"); + Slog.d(TAG, "In prepareUiData response null"); return null; } - private Intent setUpFillInIntent(@NonNull String id) { + private Intent setUpFillInIntentWithFinalRequest(@NonNull String id) { // TODO: Determine if we should skip this entry if entry id is not set, or is set // but does not resolve to a valid option. For now, not skipping it because // it may be possible that the provider adds their own extras and expects to receive // those and complete the flow. if (mBeginGetOptionToCredentialOptionMap.get(id) == null) { - Log.i(TAG, "Id from Credential Entry does not resolve to a valid option"); + Slog.w(TAG, "Id from Credential Entry does not resolve to a valid option"); return new Intent(); } return new Intent().putExtra(CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST, @@ -382,7 +380,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential getCredentialResponse); return; } - Log.i(TAG, "Pending intent response contains no credential, or error"); + Slog.d(TAG, "Pending intent response contains no credential, or error " + + "for a credential entry"); invokeCallbackOnInternalInvalidState(); } @@ -390,14 +389,12 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential private GetCredentialException maybeGetPendingIntentException( ProviderPendingIntentResponse pendingIntentResponse) { if (pendingIntentResponse == null) { - Log.i(TAG, "pendingIntentResponse is null"); return null; } if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { GetCredentialException exception = PendingIntentResultHandler .extractGetCredentialException(pendingIntentResponse.getResultData()); if (exception != null) { - Log.i(TAG, "Pending intent contains provider exception"); return exception; } } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) { @@ -463,7 +460,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** Returns true if either an exception or a response is found. */ private void onActionEntrySelected(ProviderPendingIntentResponse providerPendingIntentResponse) { - Log.i(TAG, "onActionEntrySelected"); + Slog.d(TAG, "onActionEntrySelected"); onCredentialEntrySelected(providerPendingIntentResponse); } @@ -559,7 +556,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential String id = generateUniqueId(); Entry entry = new Entry(CREDENTIAL_ENTRY_KEY, id, credentialEntry.getSlice(), - setUpFillInIntent(credentialEntry.getBeginGetCredentialOptionId())); + setUpFillInIntentWithFinalRequest(credentialEntry + .getBeginGetCredentialOptionId())); mUiCredentialEntries.put(id, new Pair<>(credentialEntry, entry)); mCredentialEntryTypes.add(credentialEntry.getType()); } @@ -574,9 +572,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential public void addAuthenticationAction(Action authenticationAction, @AuthenticationEntry.Status int status) { - Log.i(TAG, "In addAuthenticationAction"); String id = generateUniqueId(); - Log.i(TAG, "In addAuthenticationAction, id : " + id); AuthenticationEntry entry = new AuthenticationEntry( AUTHENTICATION_ACTION_ENTRY_KEY, id, authenticationAction.getSlice(), @@ -591,7 +587,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) { if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) { - Log.i(TAG, "Remote entry being dropped as it does not meet the restriction" + Slog.w(TAG, "Remote entry being dropped as it does not meet the restriction" + " checks."); return; } @@ -715,7 +711,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential == AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT) .findFirst(); if (previousMostRecentAuthEntry.isEmpty()) { - Log.i(TAG, "In updatePreviousMostRecentAuthEntry - previous entry not found"); return; } String id = previousMostRecentAuthEntry.get().getKey(); diff --git a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java index 24292ef2cdab..c10f5640c466 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java @@ -33,7 +33,7 @@ import android.os.ICancellationSignal; import android.service.credentials.CallingAppInfo; import android.service.credentials.CredentialEntry; import android.service.credentials.CredentialProviderService; -import android.telecom.Log; +import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -145,14 +145,12 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption private List<Entry> prepareUiCredentialEntries( @NonNull List<CredentialEntry> credentialEntries) { - Log.i(TAG, "in prepareUiProviderDataWithCredentials"); List<Entry> credentialUiEntries = new ArrayList<>(); // Populate the credential entries for (CredentialEntry credentialEntry : credentialEntries) { String entryId = generateUniqueId(); mUiCredentialEntries.put(entryId, credentialEntry); - Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, credentialEntry.getSlice(), setUpFillInIntent())); @@ -172,15 +170,13 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption @Override protected ProviderData prepareUiData() { - Log.i(TAG, "In prepareUiData"); if (!ProviderSession.isUiInvokingStatus(getStatus())) { - Log.i(TAG, "In prepareUiData - provider does not want to show UI: " - + mComponentName.flattenToString()); + Slog.d(TAG, "No date for UI coming from: " + mComponentName.flattenToString()); return null; } if (mProviderResponse == null) { - Log.i(TAG, "In prepareUiData response null"); - throw new IllegalStateException("Response must be in completion mode"); + Slog.w(TAG, "In prepareUiData but response is null. This is strange."); + return null; } return new GetCredentialProviderData.Builder( mComponentName.flattenToString()).setActionChips(null) @@ -200,13 +196,13 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption case CREDENTIAL_ENTRY_KEY: CredentialEntry credentialEntry = mUiCredentialEntries.get(entryKey); if (credentialEntry == null) { - Log.i(TAG, "Unexpected credential entry key"); + Slog.w(TAG, "Unexpected credential entry key"); return; } onCredentialEntrySelected(credentialEntry, providerPendingIntentResponse); break; default: - Log.i(TAG, "Unsupported entry type selected"); + Slog.w(TAG, "Unsupported entry type selected"); } } @@ -233,10 +229,8 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption } return; } - - Log.i(TAG, "Pending intent response contains no credential, or error"); } - Log.i(TAG, "CredentialEntry does not have a credential or a pending intent result"); + Slog.w(TAG, "CredentialEntry does not have a credential or a pending intent result"); } @Override @@ -279,14 +273,13 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption protected GetCredentialException maybeGetPendingIntentException( ProviderPendingIntentResponse pendingIntentResponse) { if (pendingIntentResponse == null) { - android.util.Log.i(TAG, "pendingIntentResponse is null"); return null; } if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { GetCredentialException exception = PendingIntentResultHandler .extractGetCredentialException(pendingIntentResponse.getResultData()); if (exception != null) { - android.util.Log.i(TAG, "Pending intent contains provider exception"); + Slog.d(TAG, "Pending intent contains provider exception"); return exception; } } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) { diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index d165756b3811..d02a8c1ee510 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -29,7 +29,6 @@ import android.credentials.ui.ProviderData; import android.credentials.ui.ProviderPendingIntentResponse; import android.os.ICancellationSignal; import android.os.RemoteException; -import android.util.Log; import android.util.Slog; import com.android.server.credentials.metrics.ProviderSessionMetric; @@ -253,7 +252,7 @@ public abstract class ProviderSession<T, R> @Nullable ComponentName expectedRemoteEntryProviderService) { // Check if the service is the one set by the OEM. If not silently reject this entry if (!mComponentName.equals(expectedRemoteEntryProviderService)) { - Log.i(TAG, "Remote entry being dropped as it is not from the service " + Slog.w(TAG, "Remote entry being dropped as it is not from the service " + "configured by the OEM."); return false; } @@ -270,15 +269,12 @@ public abstract class ProviderSession<T, R> return true; } } catch (SecurityException e) { - Log.i(TAG, "Error getting info for " - + mComponentName.flattenToString() + ": " + e.getMessage()); + Slog.e(TAG, "Error getting info for " + mComponentName.flattenToString(), e); return false; } catch (PackageManager.NameNotFoundException e) { - Log.i(TAG, "Error getting info for " - + mComponentName.flattenToString() + ": " + e.getMessage()); + Slog.i(TAG, "Error getting info for " + mComponentName.flattenToString(), e); return false; } - Log.i(TAG, "In enforceRemoteEntryRestrictions - remote entry checks fail"); return false; } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 492d477fe23a..b1d613109e09 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -125,6 +125,7 @@ import com.android.server.compat.PlatformCompatNative; import com.android.server.connectivity.PacProxyService; import com.android.server.contentcapture.ContentCaptureManagerInternal; import com.android.server.coverage.CoverageService; +import com.android.server.cpu.CpuMonitorService; import com.android.server.devicepolicy.DevicePolicyManagerService; import com.android.server.devicestate.DeviceStateManagerService; import com.android.server.display.DisplayManagerService; @@ -1405,6 +1406,15 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(RemoteProvisioningService.class); t.traceEnd(); + // TODO(b/277600174): Start CpuMonitorService on all builds and not just on debuggable + // builds once the Android JobScheduler starts using this service. + if (Build.IS_DEBUGGABLE || Build.IS_ENG) { + // Service for CPU monitor. + t.traceBegin("CpuMonitorService"); + mSystemServiceManager.startService(CpuMonitorService.class); + t.traceEnd(); + } + t.traceEnd(); // startCoreServices } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 8211d6fc03a2..ec177c9ac33d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -463,7 +463,8 @@ public final class BroadcastQueueModernImplTest { assertEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason()); // Bumping past barrier makes us now runnable - airplaneRecord.terminalCount++; + airplaneRecord.setDeliveryState(0, BroadcastRecord.DELIVERY_DELIVERED, + "testRunnableAt_Ordered"); queue.invalidateRunnableAt(); assertTrue(queue.isRunnable()); assertNotEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason()); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index b6bc02a41c21..d7ba3df07533 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -16,6 +16,8 @@ package com.android.server.am; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER; import static android.os.UserHandle.USER_SYSTEM; import static com.android.server.am.ActivityManagerDebugConfig.LOG_WRITER_INFO; @@ -39,7 +41,6 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -463,7 +464,7 @@ public class BroadcastQueueTest { doAnswer((invocation) -> { Log.v(TAG, "Intercepting scheduleReceiver() for " - + Arrays.toString(invocation.getArguments())); + + Arrays.toString(invocation.getArguments()) + " package " + ai.packageName); assertHealth(); final Intent intent = invocation.getArgument(0); final Bundle extras = invocation.getArgument(5); @@ -485,7 +486,7 @@ public class BroadcastQueueTest { doAnswer((invocation) -> { Log.v(TAG, "Intercepting scheduleRegisteredReceiver() for " - + Arrays.toString(invocation.getArguments())); + + Arrays.toString(invocation.getArguments()) + " package " + ai.packageName); assertHealth(); final Intent intent = invocation.getArgument(1); final Bundle extras = invocation.getArgument(4); @@ -961,7 +962,7 @@ public class BroadcastQueueTest { } else { // Confirm that app was thawed verify(mAms.mOomAdjuster, atLeastOnce()).unfreezeTemporarily( - eq(receiverApp), eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER)); + eq(receiverApp), eq(OOM_ADJ_REASON_START_RECEIVER)); // Confirm that we added package to process verify(receiverApp, atLeastOnce()).addPackage(eq(receiverApp.info.packageName), @@ -1404,7 +1405,7 @@ public class BroadcastQueueTest { // Finally, verify that we thawed the final receiver verify(mAms.mOomAdjuster).unfreezeTemporarily(eq(callerApp), - eq(OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER)); + eq(OOM_ADJ_REASON_FINISH_RECEIVER)); } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java index 2b6f2174d49b..08952eab071f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java @@ -24,7 +24,12 @@ import static android.content.Intent.ACTION_TIME_CHANGED; import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_ALL; import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_BACKGROUND_RESTRICTED_ONLY; import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_NONE; -import static com.android.server.am.BroadcastRecord.calculateBlockedUntilTerminalCount; +import static com.android.server.am.BroadcastRecord.DELIVERY_DEFERRED; +import static com.android.server.am.BroadcastRecord.DELIVERY_DELIVERED; +import static com.android.server.am.BroadcastRecord.DELIVERY_PENDING; +import static com.android.server.am.BroadcastRecord.DELIVERY_SKIPPED; +import static com.android.server.am.BroadcastRecord.DELIVERY_TIMEOUT; +import static com.android.server.am.BroadcastRecord.calculateBlockedUntilBeyondCount; import static com.android.server.am.BroadcastRecord.calculateDeferUntilActive; import static com.android.server.am.BroadcastRecord.calculateUrgent; import static com.android.server.am.BroadcastRecord.isReceiverEquals; @@ -58,7 +63,6 @@ import androidx.test.filters.SmallTest; import com.android.server.am.BroadcastDispatcher.DeferredBootCompletedBroadcastPerUser; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -79,6 +83,7 @@ import java.util.function.BiFunction; @SmallTest @RunWith(MockitoJUnitRunner.class) public class BroadcastRecordTest { + private static final String TAG = "BroadcastRecordTest"; private static final int USER0 = UserHandle.USER_SYSTEM; private static final int USER1 = USER0 + 1; @@ -120,13 +125,13 @@ public class BroadcastRecordTest { assertFalse(isPrioritized(List.of(createResolveInfo(PACKAGE1, getAppId(1), 10)))); assertArrayEquals(new int[] {-1}, - calculateBlockedUntilTerminalCount(List.of( + calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 0)), false)); assertArrayEquals(new int[] {-1}, - calculateBlockedUntilTerminalCount(List.of( + calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), -10)), false)); assertArrayEquals(new int[] {-1}, - calculateBlockedUntilTerminalCount(List.of( + calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10)), false)); } @@ -142,12 +147,12 @@ public class BroadcastRecordTest { createResolveInfo(PACKAGE3, getAppId(3), 10)))); assertArrayEquals(new int[] {-1,-1,-1}, - calculateBlockedUntilTerminalCount(List.of( + calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 0), createResolveInfo(PACKAGE2, getAppId(2), 0), createResolveInfo(PACKAGE3, getAppId(3), 0)), false)); assertArrayEquals(new int[] {-1,-1,-1}, - calculateBlockedUntilTerminalCount(List.of( + calculateBlockedUntilBeyondCount(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), createResolveInfo(PACKAGE2, getAppId(2), 10), createResolveInfo(PACKAGE3, getAppId(3), 10)), false)); @@ -156,26 +161,176 @@ public class BroadcastRecordTest { @Test public void testIsPrioritized_Yes() { assertTrue(isPrioritized(List.of( - createResolveInfo(PACKAGE1, getAppId(1), -10), + createResolveInfo(PACKAGE1, getAppId(1), 10), createResolveInfo(PACKAGE2, getAppId(2), 0), - createResolveInfo(PACKAGE3, getAppId(3), 10)))); + createResolveInfo(PACKAGE3, getAppId(3), -10)))); assertTrue(isPrioritized(List.of( - createResolveInfo(PACKAGE1, getAppId(1), 0), + createResolveInfo(PACKAGE1, getAppId(1), 10), createResolveInfo(PACKAGE2, getAppId(2), 0), - createResolveInfo(PACKAGE3, getAppId(3), 10)))); + createResolveInfo(PACKAGE3, getAppId(3), 0)))); assertArrayEquals(new int[] {0,1,2}, - calculateBlockedUntilTerminalCount(List.of( - createResolveInfo(PACKAGE1, getAppId(1), -10), + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 10), createResolveInfo(PACKAGE2, getAppId(2), 0), - createResolveInfo(PACKAGE3, getAppId(3), 10)), false)); + createResolveInfo(PACKAGE3, getAppId(3), -10)), false)); assertArrayEquals(new int[] {0,0,2,3,3}, - calculateBlockedUntilTerminalCount(List.of( - createResolveInfo(PACKAGE1, getAppId(1), 0), - createResolveInfo(PACKAGE2, getAppId(2), 0), + calculateBlockedUntilBeyondCount(List.of( + createResolveInfo(PACKAGE1, getAppId(1), 20), + createResolveInfo(PACKAGE2, getAppId(2), 20), createResolveInfo(PACKAGE3, getAppId(3), 10), - createResolveInfo(PACKAGE3, getAppId(3), 20), - createResolveInfo(PACKAGE3, getAppId(3), 20)), false)); + createResolveInfo(PACKAGE3, getAppId(3), 0), + createResolveInfo(PACKAGE3, getAppId(3), 0)), false)); + } + + @Test + public void testSetDeliveryState_Single() { + final BroadcastRecord r = createBroadcastRecord( + new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of( + createResolveInfoWithPriority(0))); + assertEquals(DELIVERY_PENDING, r.getDeliveryState(0)); + assertBlocked(r, false); + assertTerminalDeferredBeyond(r, 0, 0, 0); + + r.setDeliveryState(0, DELIVERY_DEFERRED, TAG); + assertEquals(DELIVERY_DEFERRED, r.getDeliveryState(0)); + assertBlocked(r, false); + assertTerminalDeferredBeyond(r, 0, 1, 1); + + // Identical state change has no effect + r.setDeliveryState(0, DELIVERY_DEFERRED, TAG); + assertEquals(DELIVERY_DEFERRED, r.getDeliveryState(0)); + assertBlocked(r, false); + assertTerminalDeferredBeyond(r, 0, 1, 1); + + // Moving to terminal state updates counters + r.setDeliveryState(0, DELIVERY_DELIVERED, TAG); + assertEquals(DELIVERY_DELIVERED, r.getDeliveryState(0)); + assertBlocked(r, false); + assertTerminalDeferredBeyond(r, 1, 0, 1); + + // Trying to change terminal state has no effect + r.setDeliveryState(0, DELIVERY_TIMEOUT, TAG); + assertEquals(DELIVERY_DELIVERED, r.getDeliveryState(0)); + assertBlocked(r, false); + assertTerminalDeferredBeyond(r, 1, 0, 1); + } + + @Test + public void testSetDeliveryState_Unordered() { + final BroadcastRecord r = createBroadcastRecord( + new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of( + createResolveInfoWithPriority(0), + createResolveInfoWithPriority(0), + createResolveInfoWithPriority(0))); + assertBlocked(r, false, false, false); + assertTerminalDeferredBeyond(r, 0, 0, 0); + + // Even though we finish a middle item in the tranche, we're not + // "beyond" it because there is still unfinished work before it + r.setDeliveryState(1, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false); + assertTerminalDeferredBeyond(r, 1, 0, 0); + + r.setDeliveryState(0, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false); + assertTerminalDeferredBeyond(r, 2, 0, 2); + + r.setDeliveryState(2, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false); + assertTerminalDeferredBeyond(r, 3, 0, 3); + } + + @Test + public void testSetDeliveryState_Ordered() { + final BroadcastRecord r = createOrderedBroadcastRecord( + new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of( + createResolveInfoWithPriority(0), + createResolveInfoWithPriority(0), + createResolveInfoWithPriority(0))); + assertBlocked(r, false, true, true); + assertTerminalDeferredBeyond(r, 0, 0, 0); + + r.setDeliveryState(0, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, true); + assertTerminalDeferredBeyond(r, 1, 0, 1); + + r.setDeliveryState(1, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false); + assertTerminalDeferredBeyond(r, 2, 0, 2); + + r.setDeliveryState(2, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false); + assertTerminalDeferredBeyond(r, 3, 0, 3); + } + + @Test + public void testSetDeliveryState_DeferUntilActive() { + final BroadcastRecord r = createBroadcastRecord( + new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of( + createResolveInfoWithPriority(10), + createResolveInfoWithPriority(10), + createResolveInfoWithPriority(10), + createResolveInfoWithPriority(0), + createResolveInfoWithPriority(0), + createResolveInfoWithPriority(0), + createResolveInfoWithPriority(-10), + createResolveInfoWithPriority(-10), + createResolveInfoWithPriority(-10))); + assertBlocked(r, false, false, false, true, true, true, true, true, true); + assertTerminalDeferredBeyond(r, 0, 0, 0); + + r.setDeliveryState(0, DELIVERY_PENDING, TAG); + r.setDeliveryState(1, DELIVERY_DEFERRED, TAG); + r.setDeliveryState(2, DELIVERY_PENDING, TAG); + r.setDeliveryState(3, DELIVERY_DEFERRED, TAG); + r.setDeliveryState(4, DELIVERY_DEFERRED, TAG); + r.setDeliveryState(5, DELIVERY_DEFERRED, TAG); + r.setDeliveryState(6, DELIVERY_DEFERRED, TAG); + r.setDeliveryState(7, DELIVERY_PENDING, TAG); + r.setDeliveryState(8, DELIVERY_DEFERRED, TAG); + + // Verify deferred counts ratchet up, but we're not "beyond" the first + // still-pending receiver + assertBlocked(r, false, false, false, true, true, true, true, true, true); + assertTerminalDeferredBeyond(r, 0, 6, 0); + + // We're still not "beyond" the first still-pending receiver, even when + // we finish a receiver later in the first tranche + r.setDeliveryState(2, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false, true, true, true, true, true, true); + assertTerminalDeferredBeyond(r, 1, 6, 0); + + // Completing that last item in first tranche means we now unblock the + // second tranche, and since it's entirely deferred, the third traunche + // is unblocked too + r.setDeliveryState(0, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false, false, false, false, false, false, false); + assertTerminalDeferredBeyond(r, 2, 6, 7); + + // Moving a deferred item in an earlier tranche back to being pending + // doesn't change the fact that we've already moved beyond it + r.setDeliveryState(1, DELIVERY_PENDING, TAG); + assertBlocked(r, false, false, false, false, false, false, false, false, false); + assertTerminalDeferredBeyond(r, 2, 5, 7); + r.setDeliveryState(1, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false, false, false, false, false, false, false); + assertTerminalDeferredBeyond(r, 3, 5, 7); + + // Completing middle pending item is enough to fast-forward to end + r.setDeliveryState(7, DELIVERY_DELIVERED, TAG); + assertBlocked(r, false, false, false, false, false, false, false, false, false); + assertTerminalDeferredBeyond(r, 4, 5, 9); + + // Moving everyone else directly into a finished state updates all the + // terminal counters + r.setDeliveryState(3, DELIVERY_SKIPPED, TAG); + r.setDeliveryState(4, DELIVERY_SKIPPED, TAG); + r.setDeliveryState(5, DELIVERY_SKIPPED, TAG); + r.setDeliveryState(6, DELIVERY_SKIPPED, TAG); + r.setDeliveryState(8, DELIVERY_SKIPPED, TAG); + assertBlocked(r, false, false, false, false, false, false, false, false, false); + assertTerminalDeferredBeyond(r, 9, 0, 9); } @Test @@ -688,6 +843,10 @@ public class BroadcastRecordTest { : errorMsg.insert(0, "Contains unexpected receiver: ").toString(); } + private static ResolveInfo createResolveInfoWithPriority(int priority) { + return createResolveInfo(PACKAGE1, getAppId(1), priority); + } + private static ResolveInfo createResolveInfo(String packageName, int uid) { return createResolveInfo(packageName, uid, 0); } @@ -738,21 +897,40 @@ public class BroadcastRecordTest { return excludedList; } + private BroadcastRecord createBroadcastRecord(Intent intent, + List<ResolveInfo> receivers) { + return createBroadcastRecord(receivers, USER0, intent, null /* filterExtrasForReceiver */, + null /* options */, false); + } + + private BroadcastRecord createOrderedBroadcastRecord(Intent intent, + List<ResolveInfo> receivers) { + return createBroadcastRecord(receivers, USER0, intent, null /* filterExtrasForReceiver */, + null /* options */, true); + } + private BroadcastRecord createBroadcastRecord(List<ResolveInfo> receivers, int userId, Intent intent) { return createBroadcastRecord(receivers, userId, intent, null /* filterExtrasForReceiver */, - null /* options */); + null /* options */, false); } private BroadcastRecord createBroadcastRecord(List<ResolveInfo> receivers, int userId, Intent intent, BroadcastOptions options) { return createBroadcastRecord(receivers, userId, intent, null /* filterExtrasForReceiver */, - options); + options, false); } private BroadcastRecord createBroadcastRecord(List<ResolveInfo> receivers, int userId, Intent intent, BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, BroadcastOptions options) { + return createBroadcastRecord(receivers, userId, intent, filterExtrasForReceiver, + options, false); + } + + private BroadcastRecord createBroadcastRecord(List<ResolveInfo> receivers, int userId, + Intent intent, BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, + BroadcastOptions options, boolean ordered) { return new BroadcastRecord( mQueue /* queue */, intent, @@ -774,7 +952,7 @@ public class BroadcastRecordTest { 0 /* resultCode */, null /* resultData */, null /* resultExtras */, - false /* serialized */, + ordered /* serialized */, false /* sticky */, false /* initialSticky */, userId, @@ -789,6 +967,20 @@ public class BroadcastRecordTest { private static boolean isPrioritized(List<Object> receivers) { return BroadcastRecord.isPrioritized( - calculateBlockedUntilTerminalCount(receivers, false), false); + calculateBlockedUntilBeyondCount(receivers, false), false); + } + + private static void assertBlocked(BroadcastRecord r, boolean... blocked) { + assertEquals(r.receivers.size(), blocked.length); + for (int i = 0; i < blocked.length; i++) { + assertEquals("blocked " + i, blocked[i], r.isBlocked(i)); + } + } + + private static void assertTerminalDeferredBeyond(BroadcastRecord r, + int expectedTerminalCount, int expectedDeferredCount, int expectedBeyondCount) { + assertEquals("terminal", expectedTerminalCount, r.terminalCount); + assertEquals("deferred", expectedDeferredCount, r.deferredCount); + assertEquals("beyond", expectedBeyondCount, r.beyondCount); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 485ce33dfb7d..cda5456723fb 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -38,11 +38,12 @@ import static android.app.ActivityManager.PROCESS_STATE_SERVICE; import static android.app.ActivityManager.PROCESS_STATE_TOP; import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING; import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_NONE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_ACTIVITY; import static com.android.server.am.ProcessList.BACKUP_APP_ADJ; import static com.android.server.am.ProcessList.CACHED_APP_MAX_ADJ; import static com.android.server.am.ProcessList.CACHED_APP_MIN_ADJ; @@ -254,12 +255,13 @@ public class MockingOomAdjusterTests { * - If there's only one process, then it calls updateOomAdjLocked(ProcessRecord, int). * - Otherwise, sets the processes to the LRU and run updateOomAdjLocked(int). */ + @SuppressWarnings("GuardedBy") private void updateOomAdj(ProcessRecord... apps) { if (apps.length == 1) { - sService.mOomAdjuster.updateOomAdjLocked(apps[0], OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(apps[0], OOM_ADJ_REASON_NONE); } else { setProcessesToLru(apps); - sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); sService.mProcessList.getLruProcessesLOSP().clear(); } } @@ -658,7 +660,7 @@ public class MockingOomAdjusterTests { ServiceRecord s = bindService(app, system, null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class)); sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); - sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(app, OOM_ADJ_REASON_NONE); assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND, PERCEPTIBLE_APP_ADJ + 1, SCHED_GROUP_DEFAULT); @@ -1226,7 +1228,7 @@ public class MockingOomAdjusterTests { mock(IBinder.class)); client.mState.setMaxAdj(PERSISTENT_PROC_ADJ); sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); - sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(app, OOM_ADJ_REASON_NONE); assertEquals(PERCEPTIBLE_APP_ADJ + 1, app.mState.getSetAdj()); } @@ -1243,7 +1245,7 @@ public class MockingOomAdjusterTests { mock(IBinder.class)); client.mState.setMaxAdj(PERSISTENT_PROC_ADJ); sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); - sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(app, OOM_ADJ_REASON_NONE); doReturn(false).when(wpc).isHeavyWeightProcess(); assertEquals(PERCEPTIBLE_APP_ADJ + 1, app.mState.getSetAdj()); @@ -1497,7 +1499,7 @@ public class MockingOomAdjusterTests { client2.mServices.setHasForegroundServices(false, 0, /* hasNoneType=*/false); sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); - sService.mOomAdjuster.updateOomAdjLocked(client2, OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(client2, OOM_ADJ_REASON_NONE); assertEquals(PROCESS_STATE_CACHED_EMPTY, client2.mState.getSetProcState()); assertEquals(PROCESS_STATE_CACHED_EMPTY, client.mState.getSetProcState()); @@ -1919,7 +1921,7 @@ public class MockingOomAdjusterTests { doReturn(client2).when(sService).getTopApp(); sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); - sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(app2, OOM_ADJ_REASON_NONE); assertProcStates(app2, PROCESS_STATE_BOUND_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_DEFAULT); } @@ -2029,7 +2031,7 @@ public class MockingOomAdjusterTests { setServiceMap(s3, MOCKAPP5_UID, cn3); setServiceMap(c2s, MOCKAPP3_UID, cn4); app2UidRecord.setIdle(false); - sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ, SCHED_GROUP_DEFAULT); @@ -2055,7 +2057,7 @@ public class MockingOomAdjusterTests { anyInt(), anyBoolean(), anyBoolean(), anyBoolean()); doNothing().when(sService.mServices) .scheduleServiceTimeoutLocked(any(ProcessRecord.class)); - sService.mOomAdjuster.updateOomAdjLocked(client1, OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(client1, OOM_ADJ_REASON_NONE); assertEquals(PROCESS_STATE_CACHED_EMPTY, client1.mState.getSetProcState()); assertEquals(PROCESS_STATE_SERVICE, app1.mState.getSetProcState()); @@ -2427,7 +2429,7 @@ public class MockingOomAdjusterTests { app2.mState.setHasShownUi(false); sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); - sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-ui-services"); assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj2, "cch-started-services"); @@ -2436,7 +2438,7 @@ public class MockingOomAdjusterTests { app.mState.setAdjType(null); app.mState.setSetAdj(UNKNOWN_ADJ); app.mState.setHasShownUi(false); - sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services"); @@ -2445,7 +2447,7 @@ public class MockingOomAdjusterTests { app.mState.setAdjType(null); app.mState.setSetAdj(UNKNOWN_ADJ); s.lastActivity = now - sService.mConstants.MAX_SERVICE_INACTIVITY - 1; - sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services"); @@ -2463,7 +2465,7 @@ public class MockingOomAdjusterTests { s.lastActivity = now; app.mServices.startService(s); - sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services"); assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services"); @@ -2474,7 +2476,7 @@ public class MockingOomAdjusterTests { app.mState.setSetAdj(UNKNOWN_ADJ); app.mState.setHasShownUi(false); s.lastActivity = now - sService.mConstants.MAX_SERVICE_INACTIVITY - 1; - sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services"); assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services"); @@ -2482,7 +2484,7 @@ public class MockingOomAdjusterTests { doReturn(userOther).when(sService.mUserController).getCurrentUserId(); sService.mOomAdjuster.handleUserSwitchedLocked(); - sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE); + sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services"); assertProcStates(app2, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services"); } diff --git a/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssAntennaInfoProviderTest.java b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssAntennaInfoProviderTest.java new file mode 100644 index 000000000000..e1fa8f527261 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssAntennaInfoProviderTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location.gnss; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.location.LocationManager; +import android.location.LocationManagerInternal; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; +import com.android.server.location.gnss.hal.FakeGnssHal; +import com.android.server.location.gnss.hal.GnssNative; +import com.android.server.location.injector.Injector; +import com.android.server.location.injector.TestInjector; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Objects; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class GnssAntennaInfoProviderTest { + private @Mock Context mContext; + private @Mock LocationManagerInternal mInternal; + private @Mock GnssConfiguration mMockConfiguration; + private @Mock IBinder mBinder; + private GnssNative mGnssNative; + + private GnssAntennaInfoProvider mTestProvider; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(true).when(mInternal).isProviderEnabledForUser(eq(LocationManager.GPS_PROVIDER), + anyInt()); + LocalServices.addService(LocationManagerInternal.class, mInternal); + FakeGnssHal fakeGnssHal = new FakeGnssHal(); + GnssNative.setGnssHalForTest(fakeGnssHal); + Injector injector = new TestInjector(mContext); + mGnssNative = spy(Objects.requireNonNull(GnssNative.create(injector, mMockConfiguration))); + mTestProvider = new GnssAntennaInfoProvider(mGnssNative); + mGnssNative.register(); + } + + @After + public void tearDown() { + LocalServices.removeServiceForTest(LocationManagerInternal.class); + } + + @Test + public void testOnHalStarted() { + verify(mGnssNative, times(1)).startAntennaInfoListening(); + } + + @Test + public void testOnHalRestarted() { + mTestProvider.onHalRestarted(); + verify(mGnssNative, times(2)).startAntennaInfoListening(); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssMeasurementsProviderTest.java b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssMeasurementsProviderTest.java index fd9dfe869d52..bf96b1dec4ac 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssMeasurementsProviderTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssMeasurementsProviderTest.java @@ -74,7 +74,6 @@ public class GnssMeasurementsProviderTest { private @Mock Context mContext; private @Mock LocationManagerInternal mInternal; private @Mock GnssConfiguration mMockConfiguration; - private @Mock GnssNative.GeofenceCallbacks mGeofenceCallbacks; private @Mock IGnssMeasurementsListener mListener1; private @Mock IGnssMeasurementsListener mListener2; private @Mock IBinder mBinder1; @@ -98,7 +97,6 @@ public class GnssMeasurementsProviderTest { Injector injector = new TestInjector(mContext); mGnssNative = spy(Objects.requireNonNull( GnssNative.create(injector, mMockConfiguration))); - mGnssNative.setGeofenceCallbacks(mGeofenceCallbacks); mTestProvider = new GnssMeasurementsProvider(injector, mGnssNative); mGnssNative.register(); } diff --git a/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssNavigationMessageProviderTest.java b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssNavigationMessageProviderTest.java new file mode 100644 index 000000000000..64aa4b3fa2ff --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssNavigationMessageProviderTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location.gnss; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.location.IGnssNavigationMessageListener; +import android.location.LocationManager; +import android.location.LocationManagerInternal; +import android.location.util.identity.CallerIdentity; +import android.os.IBinder; + +import com.android.server.LocalServices; +import com.android.server.location.gnss.hal.FakeGnssHal; +import com.android.server.location.gnss.hal.GnssNative; +import com.android.server.location.injector.FakeUserInfoHelper; +import com.android.server.location.injector.Injector; +import com.android.server.location.injector.TestInjector; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Objects; + +public class GnssNavigationMessageProviderTest { + private static final int CURRENT_USER = FakeUserInfoHelper.DEFAULT_USERID; + private static final CallerIdentity IDENTITY = CallerIdentity.forTest(CURRENT_USER, 1000, + "mypackage", "attribution", "listener"); + private @Mock Context mContext; + private @Mock LocationManagerInternal mInternal; + private @Mock GnssConfiguration mMockConfiguration; + private @Mock IGnssNavigationMessageListener mListener; + private @Mock IBinder mBinder; + + private GnssNative mGnssNative; + + private GnssNavigationMessageProvider mTestProvider; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mBinder).when(mListener).asBinder(); + doReturn(true).when(mInternal).isProviderEnabledForUser(eq(LocationManager.GPS_PROVIDER), + anyInt()); + LocalServices.addService(LocationManagerInternal.class, mInternal); + FakeGnssHal fakeGnssHal = new FakeGnssHal(); + GnssNative.setGnssHalForTest(fakeGnssHal); + Injector injector = new TestInjector(mContext); + mGnssNative = spy(Objects.requireNonNull(GnssNative.create(injector, mMockConfiguration))); + mTestProvider = new GnssNavigationMessageProvider(injector, mGnssNative); + mGnssNative.register(); + } + + @After + public void tearDown() { + LocalServices.removeServiceForTest(LocationManagerInternal.class); + } + + @Test + public void testAddListener() { + // add a request + mTestProvider.addListener(IDENTITY, mListener); + verify(mGnssNative, times(1)).startNavigationMessageCollection(); + + // remove a request + mTestProvider.removeListener(mListener); + verify(mGnssNative, times(1)).stopNavigationMessageCollection(); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssNmeaProviderTest.java b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssNmeaProviderTest.java new file mode 100644 index 000000000000..49e5e69933f9 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssNmeaProviderTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location.gnss; + + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.location.IGnssNmeaListener; +import android.location.LocationManager; +import android.location.LocationManagerInternal; +import android.location.util.identity.CallerIdentity; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.server.LocalServices; +import com.android.server.location.gnss.hal.FakeGnssHal; +import com.android.server.location.gnss.hal.GnssNative; +import com.android.server.location.injector.FakeUserInfoHelper; +import com.android.server.location.injector.Injector; +import com.android.server.location.injector.TestInjector; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Objects; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class GnssNmeaProviderTest { + + private static final int CURRENT_USER = FakeUserInfoHelper.DEFAULT_USERID; + private static final CallerIdentity IDENTITY = CallerIdentity.forTest(CURRENT_USER, 1000, + "mypackage", "attribution", "listener"); + private @Mock Context mContext; + private @Mock LocationManagerInternal mInternal; + private @Mock GnssConfiguration mMockConfiguration; + private @Mock IGnssNmeaListener mListener; + private @Mock IBinder mBinder; + + private GnssNative mGnssNative; + + private GnssNmeaProvider mTestProvider; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mBinder).when(mListener).asBinder(); + doReturn(true).when(mInternal).isProviderEnabledForUser(eq(LocationManager.GPS_PROVIDER), + anyInt()); + LocalServices.addService(LocationManagerInternal.class, mInternal); + FakeGnssHal fakeGnssHal = new FakeGnssHal(); + GnssNative.setGnssHalForTest(fakeGnssHal); + Injector injector = new TestInjector(mContext); + mGnssNative = spy(Objects.requireNonNull(GnssNative.create(injector, mMockConfiguration))); + mTestProvider = new GnssNmeaProvider(injector, mGnssNative); + mGnssNative.register(); + } + + @After + public void tearDown() { + LocalServices.removeServiceForTest(LocationManagerInternal.class); + } + + @Test + public void testAddListener() { + // add a request + mTestProvider.addListener(IDENTITY, mListener); + verify(mGnssNative, times(1)).startNmeaMessageCollection(); + + // remove a request + mTestProvider.removeListener(mListener); + verify(mGnssNative, times(1)).stopNmeaMessageCollection(); + } + +} diff --git a/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssStatusProviderTest.java b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssStatusProviderTest.java new file mode 100644 index 000000000000..ce2aec7f8d5d --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/location/gnss/GnssStatusProviderTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location.gnss; + + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.location.IGnssStatusListener; +import android.location.LocationManager; +import android.location.LocationManagerInternal; +import android.location.util.identity.CallerIdentity; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.server.LocalServices; +import com.android.server.location.gnss.hal.FakeGnssHal; +import com.android.server.location.gnss.hal.GnssNative; +import com.android.server.location.injector.FakeUserInfoHelper; +import com.android.server.location.injector.Injector; +import com.android.server.location.injector.TestInjector; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Objects; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class GnssStatusProviderTest { + private static final int CURRENT_USER = FakeUserInfoHelper.DEFAULT_USERID; + private static final CallerIdentity IDENTITY = CallerIdentity.forTest(CURRENT_USER, 1000, + "mypackage", "attribution", "listener"); + private @Mock Context mContext; + private @Mock LocationManagerInternal mInternal; + private @Mock GnssConfiguration mMockConfiguration; + private @Mock IGnssStatusListener mListener; + private @Mock IBinder mBinder; + + private GnssNative mGnssNative; + + private GnssStatusProvider mTestProvider; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mBinder).when(mListener).asBinder(); + doReturn(true).when(mInternal).isProviderEnabledForUser(eq(LocationManager.GPS_PROVIDER), + anyInt()); + LocalServices.addService(LocationManagerInternal.class, mInternal); + FakeGnssHal fakeGnssHal = new FakeGnssHal(); + GnssNative.setGnssHalForTest(fakeGnssHal); + Injector injector = new TestInjector(mContext); + mGnssNative = spy(Objects.requireNonNull(GnssNative.create(injector, mMockConfiguration))); + mTestProvider = new GnssStatusProvider(injector, mGnssNative); + mGnssNative.register(); + } + + @After + public void tearDown() { + LocalServices.removeServiceForTest(LocationManagerInternal.class); + } + + @Test + public void testAddListener() { + // add a request + mTestProvider.addListener(IDENTITY, mListener); + verify(mGnssNative, times(1)).startSvStatusCollection(); + + // remove a request + mTestProvider.removeListener(mListener); + verify(mGnssNative, times(1)).stopSvStatusCollection(); + } + +} diff --git a/services/tests/mockingservicestests/src/com/android/server/location/gnss/hal/FakeGnssHal.java b/services/tests/mockingservicestests/src/com/android/server/location/gnss/hal/FakeGnssHal.java index b7ab6f80167e..2d962acfe665 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/gnss/hal/FakeGnssHal.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/gnss/hal/FakeGnssHal.java @@ -562,6 +562,26 @@ public final class FakeGnssHal extends GnssNative.GnssHal { } @Override + protected boolean startSvStatusCollection() { + return true; + } + + @Override + protected boolean stopSvStatusCollection() { + return true; + } + + @Override + public boolean startNmeaMessageCollection() { + return true; + } + + @Override + public boolean stopNmeaMessageCollection() { + return true; + } + + @Override protected int getBatchSize() { return mBatchSize; } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index dbf5021d3c6b..26a3ae110525 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -43,6 +43,7 @@ import android.annotation.NonNull; import android.app.admin.DevicePolicyManager; import android.app.trust.ITrustManager; import android.content.Context; +import android.content.res.Resources; import android.hardware.biometrics.BiometricManager.Authenticators; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricSensorReceiver; @@ -52,6 +53,7 @@ import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorProperties; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Binder; @@ -83,6 +85,7 @@ public class AuthSessionTest { private static final long TEST_REQUEST_ID = 22; @Mock private Context mContext; + @Mock private Resources mResources; @Mock private BiometricContext mBiometricContext; @Mock private ITrustManager mTrustManager; @Mock private DevicePolicyManager mDevicePolicyManager; @@ -104,6 +107,7 @@ public class AuthSessionTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + when(mContext.getResources()).thenReturn(mResources); when(mClientReceiver.asBinder()).thenReturn(mock(Binder.class)); when(mBiometricContext.updateContext(any(), anyBoolean())) .thenAnswer(invocation -> invocation.getArgument(0)); @@ -342,6 +346,33 @@ public class AuthSessionTest { testInvokesCancel(session -> session.onDialogDismissed(DISMISSED_REASON_NEGATIVE, null)); } + @Test + public void testCallbackOnAcquired() throws RemoteException { + final String acquiredStr = "test_acquired_info_callback"; + final String acquiredStrVendor = "test_acquired_info_callback_vendor"; + setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_REAR); + + final AuthSession session = createAuthSession(mSensors, + false /* checkDevicePolicyManager */, + Authenticators.BIOMETRIC_STRONG, + TEST_REQUEST_ID, + 0 /* operationId */, + 0 /* userId */); + + when(mContext.getString(com.android.internal.R.string.fingerprint_acquired_partial)) + .thenReturn(acquiredStr); + session.onAcquired(0, FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL, 0); + verify(mStatusBarService).onBiometricHelp(anyInt(), eq(acquiredStr)); + verify(mClientReceiver).onAcquired(eq(1), eq(acquiredStr)); + + when(mResources.getStringArray(com.android.internal.R.array.fingerprint_acquired_vendor)) + .thenReturn(new String[]{acquiredStrVendor}); + session.onAcquired(0, FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 0); + verify(mStatusBarService).onBiometricHelp(anyInt(), eq(acquiredStrVendor)); + verify(mClientReceiver).onAcquired( + eq(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR_BASE), eq(acquiredStrVendor)); + } + // TODO (b/208484275) : Enable these tests // @Test // public void testPreAuth_canAuthAndPrivacyDisabled() throws Exception { diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index 550204b99323..4cfbb9520d5f 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -105,6 +105,7 @@ public class LocaleManagerServiceTest { mMockPackageManager = mock(PackageManager.class); mMockPackageMonitor = mock(PackageMonitor.class); + doReturn(mMockContext).when(mMockContext).createContextAsUser(any(), anyInt()); // For unit tests, set the default installer info doReturn(DEFAULT_INSTALL_SOURCE_INFO).when(mMockPackageManager) .getInstallSourceInfo(anyString()); diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java index da9de2562930..e20f1e7065d4 100644 --- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java @@ -131,6 +131,7 @@ public class SystemAppUpdateTrackerTest { doReturn(mMockPackageManager).when(mMockContext).getPackageManager(); doReturn(InstrumentationRegistry.getContext().getContentResolver()) .when(mMockContext).getContentResolver(); + doReturn(mMockContext).when(mMockContext).createContextAsUser(any(), anyInt()); mStoragefile = new AtomicFile(new File( Environment.getExternalStorageDirectory(), "systemUpdateUnitTests.xml")); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java new file mode 100644 index 000000000000..bcd807ab6d2f --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static junit.framework.Assert.assertEquals; +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 junit.framework.Assert.fail; + +import android.content.ComponentName; +import android.net.Uri; +import android.provider.Settings; +import android.service.notification.Condition; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeDiff; +import android.service.notification.ZenPolicy; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.ArrayMap; + +import androidx.test.filters.SmallTest; + +import com.android.server.UiServiceTestCase; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class ZenModeDiffTest extends UiServiceTestCase { + // version is not included in the diff; manual & automatic rules have special handling + public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS = + Set.of("version", "manualRule", "automaticRules"); + + @Test + public void testRuleDiff_addRemoveSame() { + // Test add, remove, and both sides same + ZenModeConfig.ZenRule r = makeRule(); + + // Both sides same rule + ZenModeDiff.RuleDiff dSame = new ZenModeDiff.RuleDiff(r, r); + assertFalse(dSame.hasDiff()); + + // from existent rule to null: expect deleted + ZenModeDiff.RuleDiff deleted = new ZenModeDiff.RuleDiff(r, null); + assertTrue(deleted.hasDiff()); + assertTrue(deleted.wasRemoved()); + + // from null to new rule: expect added + ZenModeDiff.RuleDiff added = new ZenModeDiff.RuleDiff(null, r); + assertTrue(added.hasDiff()); + assertTrue(added.wasAdded()); + } + + @Test + public void testRuleDiff_fieldDiffs() throws Exception { + // Start these the same + ZenModeConfig.ZenRule r1 = makeRule(); + ZenModeConfig.ZenRule r2 = makeRule(); + + // maps mapping field name -> expected output value as we set diffs + ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); + ArrayMap<String, Object> expectedTo = new ArrayMap<>(); + List<Field> fieldsForDiff = getFieldsForDiffCheck( + ZenModeConfig.ZenRule.class, Set.of()); // actually no exempt fields for ZenRule + generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo); + + ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2); + assertTrue(d.hasDiff()); + + // Now diff them and check that each of the fields has a diff + for (Field f : fieldsForDiff) { + String name = f.getName(); + assertNotNull("diff not found for field: " + name, d.getDiffForField(name)); + assertTrue(d.getDiffForField(name).hasDiff()); + assertTrue("unexpected field: " + name, expectedFrom.containsKey(name)); + assertTrue("unexpected field: " + name, expectedTo.containsKey(name)); + assertEquals(expectedFrom.get(name), d.getDiffForField(name).from()); + assertEquals(expectedTo.get(name), d.getDiffForField(name).to()); + } + } + + @Test + public void testConfigDiff_addRemoveSame() { + // Default config, will test add, remove, and no change + ZenModeConfig c = new ZenModeConfig(); + + ZenModeDiff.ConfigDiff dSame = new ZenModeDiff.ConfigDiff(c, c); + assertFalse(dSame.hasDiff()); + + ZenModeDiff.ConfigDiff added = new ZenModeDiff.ConfigDiff(null, c); + assertTrue(added.hasDiff()); + assertTrue(added.wasAdded()); + + ZenModeDiff.ConfigDiff removed = new ZenModeDiff.ConfigDiff(c, null); + assertTrue(removed.hasDiff()); + assertTrue(removed.wasRemoved()); + } + + @Test + public void testConfigDiff_fieldDiffs() throws Exception { + // these two start the same + ZenModeConfig c1 = new ZenModeConfig(); + ZenModeConfig c2 = new ZenModeConfig(); + + // maps mapping field name -> expected output value as we set diffs + ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); + ArrayMap<String, Object> expectedTo = new ArrayMap<>(); + List<Field> fieldsForDiff = getFieldsForDiffCheck( + ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS); + generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo); + + ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2); + assertTrue(d.hasDiff()); + + // Now diff them and check that each of the fields has a diff + for (Field f : fieldsForDiff) { + String name = f.getName(); + assertNotNull("diff not found for field: " + name, d.getDiffForField(name)); + assertTrue(d.getDiffForField(name).hasDiff()); + assertTrue("unexpected field: " + name, expectedFrom.containsKey(name)); + assertTrue("unexpected field: " + name, expectedTo.containsKey(name)); + assertEquals(expectedFrom.get(name), d.getDiffForField(name).from()); + assertEquals(expectedTo.get(name), d.getDiffForField(name).to()); + } + } + + @Test + public void testConfigDiff_specialSenders() { + // these two start the same + ZenModeConfig c1 = new ZenModeConfig(); + ZenModeConfig c2 = new ZenModeConfig(); + + // set c1 and c2 to have some different senders + c1.allowMessagesFrom = ZenModeConfig.SOURCE_STAR; + c2.allowMessagesFrom = ZenModeConfig.SOURCE_CONTACT; + c1.allowConversationsFrom = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; + c2.allowConversationsFrom = ZenPolicy.CONVERSATION_SENDERS_NONE; + + ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2); + assertTrue(d.hasDiff()); + + // Diff in top-level fields + assertTrue(d.getDiffForField("allowMessagesFrom").hasDiff()); + assertTrue(d.getDiffForField("allowConversationsFrom").hasDiff()); + + // Bonus testing of stringification of people senders and conversation senders + assertTrue(d.toString().contains("allowMessagesFrom:stars->contacts")); + assertTrue(d.toString().contains("allowConversationsFrom:important->none")); + } + + @Test + public void testConfigDiff_hasRuleDiffs() { + // two default configs + ZenModeConfig c1 = new ZenModeConfig(); + ZenModeConfig c2 = new ZenModeConfig(); + + // two initially-identical rules + ZenModeConfig.ZenRule r1 = makeRule(); + ZenModeConfig.ZenRule r2 = makeRule(); + + // one that will become a manual rule + ZenModeConfig.ZenRule m = makeRule(); + + // Add r1 to c1, but not r2 to c2 yet -- expect a rule to be deleted + c1.automaticRules.put(r1.id, r1); + ZenModeDiff.ConfigDiff deleteRule = new ZenModeDiff.ConfigDiff(c1, c2); + assertTrue(deleteRule.hasDiff()); + assertNotNull(deleteRule.getAllAutomaticRuleDiffs()); + assertTrue(deleteRule.getAllAutomaticRuleDiffs().containsKey("ruleId")); + assertTrue(deleteRule.getAllAutomaticRuleDiffs().get("ruleId").wasRemoved()); + + // Change r2 a little, add r2 to c2 as an automatic rule and m as a manual rule + r2.component = null; + r2.pkg = "different"; + c2.manualRule = m; + c2.automaticRules.put(r2.id, r2); + + // Expect diffs in both manual rule (added) and automatic rule (changed) + ZenModeDiff.ConfigDiff changed = new ZenModeDiff.ConfigDiff(c1, c2); + assertTrue(changed.hasDiff()); + assertTrue(changed.getManualRuleDiff().hasDiff()); + + ArrayMap<String, ZenModeDiff.RuleDiff> automaticDiffs = changed.getAllAutomaticRuleDiffs(); + assertNotNull(automaticDiffs); + assertTrue(automaticDiffs.containsKey("ruleId")); + assertNotNull(automaticDiffs.get("ruleId").getDiffForField("component")); + assertNull(automaticDiffs.get("ruleId").getDiffForField("component").to()); + assertNotNull(automaticDiffs.get("ruleId").getDiffForField("pkg")); + assertEquals("different", automaticDiffs.get("ruleId").getDiffForField("pkg").to()); + } + + // Helper methods for working with configs, policies, rules + // Just makes a zen rule with fields filled in + private ZenModeConfig.ZenRule makeRule() { + ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); + rule.configurationActivity = new ComponentName("a", "a"); + rule.component = new ComponentName("b", "b"); + rule.conditionId = new Uri.Builder().scheme("hello").build(); + rule.condition = new Condition(rule.conditionId, "", Condition.STATE_TRUE); + rule.enabled = true; + rule.creationTime = 123; + rule.id = "ruleId"; + rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + rule.modified = false; + rule.name = "name"; + rule.snoozing = true; + rule.pkg = "a"; + return rule; + } + + // Get the fields on which we would want to check a diff. The requirements are: not final or/ + // static (as these should/can never change), and not in a specific list that's exempted. + private List<Field> getFieldsForDiffCheck(Class c, Set<String> exemptNames) + throws SecurityException { + Field[] fields = c.getDeclaredFields(); + ArrayList<Field> out = new ArrayList<>(); + + for (Field field : fields) { + // Check for exempt reasons + int m = field.getModifiers(); + if (Modifier.isFinal(m) + || Modifier.isStatic(m) + || exemptNames.contains(field.getName())) { + continue; + } + out.add(field); + } + return out; + } + + // Generate a set of generic diffs for the specified two objects and the fields to generate + // diffs for, and store the results in the provided expectation maps to be able to check the + // output later. + private void generateFieldDiffs(Object a, Object b, List<Field> fields, + ArrayMap<String, Object> expectedA, ArrayMap<String, Object> expectedB) + throws Exception { + // different classes passed in means bad input + assertEquals(a.getClass(), b.getClass()); + + // Loop through fields for which we want to check diffs, set a diff and keep track of + // what we set. + for (Field f : fields) { + f.setAccessible(true); + // Just double-check also that the fields actually are for the class declared + assertEquals(f.getDeclaringClass(), a.getClass()); + Class t = f.getType(); + // handle the full set of primitive types first + if (boolean.class.equals(t)) { + f.setBoolean(a, true); + expectedA.put(f.getName(), true); + f.setBoolean(b, false); + expectedB.put(f.getName(), false); + } else if (int.class.equals(t)) { + // these are not actually valid going to be valid for arbitrary int enum fields, but + // we just put something in there regardless. + f.setInt(a, 2); + expectedA.put(f.getName(), 2); + f.setInt(b, 1); + expectedB.put(f.getName(), 1); + } else if (long.class.equals(t)) { + f.setLong(a, 200L); + expectedA.put(f.getName(), 200L); + f.setLong(b, 100L); + expectedB.put(f.getName(), 100L); + } else if (t.isPrimitive()) { + // This method doesn't yet handle other primitive types. If the relevant diff + // classes gain new fields of these types, please add another clause here. + fail("primitive type not handled by generateFieldDiffs: " + t.getName()); + } else if (String.class.equals(t)) { + f.set(a, "string1"); + expectedA.put(f.getName(), "string1"); + f.set(b, "string2"); + expectedB.put(f.getName(), "string2"); + } else { + // catch-all for other types: have the field be "added" + f.set(a, null); + expectedA.put(f.getName(), null); + try { + f.set(b, t.getDeclaredConstructor().newInstance()); + expectedB.put(f.getName(), t.getDeclaredConstructor().newInstance()); + } catch (Exception e) { + // No default constructor, or blithely attempting to construct something doesn't + // work for some reason. If the default value isn't null, then keep it. + if (f.get(b) != null) { + expectedB.put(f.getName(), f.get(b)); + } else { + // If we can't even rely on that, fail. Have the test-writer special case + // something, as this is not able to be genericized. + fail("could not generically construct value for field: " + f.getName()); + } + } + } + } + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 6f9798ea7d69..b2a54010e75e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -69,8 +69,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.AutomaticZenRule; @@ -78,7 +76,6 @@ import android.app.NotificationManager; import android.app.NotificationManager.Policy; import android.content.ComponentName; import android.content.ContentResolver; -import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -90,7 +87,6 @@ import android.media.AudioManagerInternal; import android.media.AudioSystem; import android.media.VolumePolicy; import android.net.Uri; -import android.os.Binder; import android.os.Process; import android.os.UserHandle; import android.provider.Settings; @@ -98,6 +94,7 @@ import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ScheduleInfo; +import android.service.notification.ZenModeDiff; import android.service.notification.ZenPolicy; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; @@ -877,7 +874,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelperSpy.readXml(parser, false, UserHandle.USER_ALL); assertEquals("Config mismatch: current vs expected: " - + mZenModeHelperSpy.mConfig.diff(expected), expected, mZenModeHelperSpy.mConfig); + + new ZenModeDiff.ConfigDiff(mZenModeHelperSpy.mConfig, expected), expected, + mZenModeHelperSpy.mConfig); } @Test @@ -1046,7 +1044,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig actual = mZenModeHelperSpy.mConfigs.get(10); assertEquals( - "Config mismatch: current vs expected: " + actual.diff(config10), config10, actual); + "Config mismatch: current vs expected: " + + new ZenModeDiff.ConfigDiff(actual, config10), config10, actual); assertNotEquals("Expected config mismatch", config11, mZenModeHelperSpy.mConfigs.get(11)); } @@ -1062,7 +1061,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelperSpy.readXml(parser, true, UserHandle.USER_SYSTEM); assertEquals("Config mismatch: current vs original: " - + mZenModeHelperSpy.mConfig.diff(original), original, mZenModeHelperSpy.mConfig); + + new ZenModeDiff.ConfigDiff(mZenModeHelperSpy.mConfig, original), + original, mZenModeHelperSpy.mConfig); assertEquals(original.hashCode(), mZenModeHelperSpy.mConfig.hashCode()); } @@ -1083,8 +1083,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig actual = mZenModeHelperSpy.mConfigs.get(10); expected.user = 10; - assertEquals( - "Config mismatch: current vs original: " + actual.diff(expected), expected, actual); + assertEquals("Config mismatch: current vs original: " + + new ZenModeDiff.ConfigDiff(actual, expected), + expected, actual); assertEquals(expected.hashCode(), actual.hashCode()); expected.user = 0; assertNotEquals(expected, mZenModeHelperSpy.mConfig); diff --git a/services/tests/voiceinteractiontests/Android.bp b/services/tests/voiceinteractiontests/Android.bp index 986fb71afa2d..e704ebf32270 100644 --- a/services/tests/voiceinteractiontests/Android.bp +++ b/services/tests/voiceinteractiontests/Android.bp @@ -40,6 +40,7 @@ android_test { "platform-test-annotations", "services.core", "services.voiceinteraction", + "services.soundtrigger", "servicestests-core-utils", "servicestests-utils-mockito-extended", "truth-prebuilt", diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 341b331b74e0..8f2b470908c4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -83,6 +83,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.server.wm.ActivityRecord.FINISH_RESULT_CANCELLED; import static com.android.server.wm.ActivityRecord.FINISH_RESULT_REMOVED; import static com.android.server.wm.ActivityRecord.FINISH_RESULT_REQUESTED; +import static com.android.server.wm.ActivityRecord.LAUNCH_SOURCE_TYPE_HOME; import static com.android.server.wm.ActivityRecord.State.DESTROYED; import static com.android.server.wm.ActivityRecord.State.DESTROYING; import static com.android.server.wm.ActivityRecord.State.FINISHING; @@ -3688,6 +3689,23 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(activity.inTransition()); } + /** + * Verifies the task is moved to back when back pressed if the root activity was originally + * started from Launcher. + */ + @Test + public void testMoveTaskToBackWhenStartedFromLauncher() { + final Task task = createTask(mDisplayContent); + final ActivityRecord ar = createActivityRecord(task); + task.realActivity = ar.mActivityComponent; + ar.intent.setAction(Intent.ACTION_MAIN); + ar.intent.addCategory(Intent.CATEGORY_LAUNCHER); + doReturn(true).when(ar).isLaunchSourceType(eq(LAUNCH_SOURCE_TYPE_HOME)); + + mAtm.mActivityClientController.onBackPressed(ar.token, null /* callback */); + verify(task).moveTaskToBack(any()); + } + private ICompatCameraControlCallback getCompatCameraControlCallback() { return new ICompatCameraControlCallback.Stub() { @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java index 8e91ca28fcf1..77efc4b0d561 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java @@ -42,6 +42,8 @@ import android.view.SurfaceControl; import androidx.test.filters.SmallTest; +import com.android.server.testutils.TestHandler; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -371,6 +373,49 @@ public class SyncEngineTests extends WindowTestsBase { mAppWindow.removeImmediately(); } + @Test + public void testQueueSyncSet() { + final TestHandler testHandler = new TestHandler(null); + TestWindowContainer mockWC = new TestWindowContainer(mWm, true /* waiter */); + TestWindowContainer mockWC2 = new TestWindowContainer(mWm, true /* waiter */); + + final BLASTSyncEngine bse = createTestBLASTSyncEngine(testHandler); + + BLASTSyncEngine.TransactionReadyListener listener = mock( + BLASTSyncEngine.TransactionReadyListener.class); + + int id = startSyncSet(bse, listener); + bse.addToSyncSet(id, mockWC); + bse.setReady(id); + bse.onSurfacePlacement(); + verify(listener, times(0)).onTransactionReady(eq(id), notNull()); + + final int[] nextId = new int[]{-1}; + bse.queueSyncSet( + () -> nextId[0] = startSyncSet(bse, listener), + () -> { + bse.setReady(nextId[0]); + bse.addToSyncSet(nextId[0], mockWC2); + }); + + // Make sure it is queued + assertEquals(-1, nextId[0]); + + // Finish the original sync and see that we've started a new sync-set immediately but + // that the readiness was posted. + mockWC.onSyncFinishedDrawing(); + verify(mWm.mWindowPlacerLocked).requestTraversal(); + bse.onSurfacePlacement(); + verify(listener, times(1)).onTransactionReady(eq(id), notNull()); + + assertTrue(nextId[0] != -1); + assertFalse(bse.isReady(nextId[0])); + + // now make sure the applySync callback was posted. + testHandler.flush(); + assertTrue(bse.isReady(nextId[0])); + } + static int startSyncSet(BLASTSyncEngine engine, BLASTSyncEngine.TransactionReadyListener listener) { return engine.startSyncSet(listener, BLAST_TIMEOUT_DURATION, "Test"); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 90506d4f8651..1e2fdec07d00 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1404,8 +1404,6 @@ public class TransitionTests extends WindowTestsBase { // We are now going to simulate closing task1 to return back to (open) task2. final Transition closeTransition = controller.createTransition(TRANSIT_CLOSE); - closeTransition.collectExistenceChange(task1); - closeTransition.collectExistenceChange(activity1); closeTransition.collectExistenceChange(task2); closeTransition.collectExistenceChange(activity2); closeTransition.setTransientLaunch(activity2, task1); @@ -1416,7 +1414,7 @@ public class TransitionTests extends WindowTestsBase { assertNotNull(activity1ChangeInfo); assertTrue(activity1ChangeInfo.hasChanged()); // No need to wait for the activity in transient hide task. - assertTrue(activity1.isSyncFinished()); + assertEquals(WindowContainer.SYNC_STATE_NONE, activity1.mSyncState); activity1.setVisibleRequested(false); activity2.setVisibleRequested(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 7e3ec55f262a..f85cdf0b5035 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -77,6 +77,7 @@ import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; @@ -886,7 +887,11 @@ class WindowTestsBase extends SystemServiceTestsBase { } BLASTSyncEngine createTestBLASTSyncEngine() { - return new BLASTSyncEngine(mWm) { + return createTestBLASTSyncEngine(mWm.mH); + } + + BLASTSyncEngine createTestBLASTSyncEngine(Handler handler) { + return new BLASTSyncEngine(mWm, handler) { @Override void scheduleTimeout(SyncGroup s, long timeoutMs) { // Disable timeout. diff --git a/services/voiceinteraction/Android.bp b/services/voiceinteraction/Android.bp index 7332d2d8b0f6..de8d1440e6ac 100644 --- a/services/voiceinteraction/Android.bp +++ b/services/voiceinteraction/Android.bp @@ -9,11 +9,60 @@ package { filegroup { name: "services.voiceinteraction-sources", - srcs: ["java/**/*.java"], + srcs: ["java/com/android/server/voiceinteraction/*.java"], path: "java", visibility: ["//frameworks/base/services"], } +filegroup { + name: "services.soundtrigger_middleware-sources", + srcs: ["java/com/android/server/soundtrigger_middleware/*.java"], + path: "java", + visibility: ["//visibility:private"], +} + +filegroup { + name: "services.soundtrigger_service-sources", + srcs: ["java/com/android/server/soundtrigger/*.java"], + path: "java", + visibility: ["//visibility:private"], +} + +filegroup { + name: "services.soundtrigger-sources", + srcs: [ + ":services.soundtrigger_service-sources", + ":services.soundtrigger_middleware-sources", + ], + path: "java", + visibility: ["//frameworks/base/services"], +} + +java_library_static { + name: "services.soundtrigger_middleware", + defaults: ["platform_service_defaults"], + srcs: [":services.soundtrigger_middleware-sources"], + libs: [ + "services.core", + ], + static_libs: [ + "android.hardware.soundtrigger-V2.3-java", + ], + visibility: ["//visibility/base/services/tests/voiceinteraction"], +} + +java_library_static { + name: "services.soundtrigger", + defaults: ["platform_service_defaults"], + srcs: [":services.soundtrigger_service-sources"], + libs: [ + "services.core", + ], + static_libs: [ + "services.soundtrigger_middleware", + ], +} + java_library_static { name: "services.voiceinteraction", defaults: ["platform_service_defaults"], diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java index 04c1c0451e63..203a3e74d9da 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java @@ -39,12 +39,13 @@ import android.app.ActivityThread; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.PermissionChecker; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.hardware.soundtrigger.ConversionUtil; import android.hardware.soundtrigger.IRecognitionStatusCallback; import android.hardware.soundtrigger.ModelParams; -import android.hardware.soundtrigger.ConversionUtil; import android.hardware.soundtrigger.SoundTrigger; import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel; import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; @@ -64,6 +65,7 @@ import android.media.permission.SafeCloseable; import android.media.soundtrigger.ISoundTriggerDetectionService; import android.media.soundtrigger.ISoundTriggerDetectionServiceClient; import android.media.soundtrigger.SoundTriggerDetectionService; +import android.media.soundtrigger_middleware.ISoundTriggerInjection; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.os.Binder; import android.os.Bundle; @@ -74,8 +76,8 @@ import android.os.Parcel; import android.os.ParcelUuid; import android.os.PowerManager; import android.os.RemoteException; -import android.os.ServiceSpecificException; import android.os.ServiceManager; +import android.os.ServiceSpecificException; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -86,6 +88,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.ISoundTriggerService; import com.android.internal.app.ISoundTriggerSession; +import com.android.server.SoundTriggerInternal; import com.android.server.SystemService; import com.android.server.utils.EventLogger; @@ -98,8 +101,8 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.UUID; -import java.util.stream.Collectors; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * A single SystemService to manage all sound/voice-based sound models on the DSP. @@ -296,6 +299,23 @@ public class SoundTriggerService extends SystemService { return listUnderlyingModuleProperties(originatorIdentity); } } + + @Override + public void attachInjection(@NonNull ISoundTriggerInjection injection) { + if (PermissionChecker.checkCallingPermissionForPreflight(mContext, + android.Manifest.permission.MANAGE_SOUND_TRIGGER, null) + != PermissionChecker.PERMISSION_GRANTED) { + throw new SecurityException(); + } + try { + ISoundTriggerMiddlewareService.Stub + .asInterface(ServiceManager + .waitForService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE)) + .attachFakeHalInjection(injection); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } class SoundTriggerSessionStub extends ISoundTriggerSession.Stub { diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java index aaf7a9eacce6..5846ff699f69 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java @@ -39,7 +39,7 @@ import java.util.UUID; * * @hide */ -public class DatabaseHelper extends SQLiteOpenHelper { +public class DatabaseHelper extends SQLiteOpenHelper implements IEnrolledModelDb { static final String TAG = "SoundModelDBHelper"; static final boolean DBG = false; @@ -153,11 +153,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } } - /** - * Updates the given keyphrase model, adds it, if it doesn't already exist. - * - * TODO: We only support one keyphrase currently. - */ + @Override public boolean updateKeyphraseSoundModel(KeyphraseSoundModel soundModel) { synchronized(this) { SQLiteDatabase db = getWritableDatabase(); @@ -193,9 +189,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } } - /** - * Deletes the sound model and associated keyphrases. - */ + @Override public boolean deleteKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale) { // Normalize the locale to guard against SQL injection. bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag(); @@ -218,12 +212,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } } - /** - * Returns a matching {@link KeyphraseSoundModel} for the keyphrase ID. - * Returns null if a match isn't found. - * - * TODO: We only support one keyphrase currently. - */ + @Override public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale) { // Sanitize the locale to guard against SQL injection. @@ -237,12 +226,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } } - /** - * Returns a matching {@link KeyphraseSoundModel} for the keyphrase string. - * Returns null if a match isn't found. - * - * TODO: We only support one keyphrase currently. - */ + @Override public KeyphraseSoundModel getKeyphraseSoundModel(String keyphrase, int userHandle, String bcp47Locale) { // Sanitize the locale to guard against SQL injection. diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/IEnrolledModelDb.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/IEnrolledModelDb.java new file mode 100644 index 000000000000..f10c2f6a2325 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/IEnrolledModelDb.java @@ -0,0 +1,90 @@ +/** + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.voiceinteraction; + +import android.hardware.soundtrigger.SoundTrigger.Keyphrase; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; + +import java.io.PrintWriter; + +/** + * Interface for registering and querying the enrolled keyphrase model database for + * {@link VoiceInteractionManagerService}. + * This interface only supports one keyphrase per {@link KeyphraseSoundModel}. + * The non-update methods are uniquely keyed on fields of the first keyphrase + * {@link KeyphraseSoundModel#getKeyphrases()}. + * @hide + */ +public interface IEnrolledModelDb { + + //TODO(273286174): We only support one keyphrase currently. + /** + * Register the given {@link KeyphraseSoundModel}, or updates it if it already exists. + * + * @param soundModel - The sound model to register in the database. + * Updates the sound model if the keyphrase id, users, locale match an existing entry. + * Must have one and only one associated {@link Keyphrase}. + * @return - {@code true} if successful, {@code false} if unsuccessful + */ + boolean updateKeyphraseSoundModel(KeyphraseSoundModel soundModel); + + /** + * Deletes the previously registered keyphrase sound model from the database. + * + * @param keyphraseId - The (first) keyphrase ID of the KeyphraseSoundModel to delete. + * @param userHandle - The user handle making this request. Must be included in the user + * list of the registered sound model. + * @param bcp47Locale - The locale of the (first) keyphrase associated with this model. + * @return - {@code true} if successful, {@code false} if unsuccessful + */ + boolean deleteKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale); + + //TODO(273286174): We only support one keyphrase currently. + /** + * Returns the first matching {@link KeyphraseSoundModel} for the keyphrase ID, locale pair, + * contingent on the userHandle existing in the user list for the model. + * Returns null if a match isn't found. + * + * @param keyphraseId - The (first) keyphrase ID of the KeyphraseSoundModel to query. + * @param userHandle - The user handle making this request. Must be included in the user + * list of the registered sound model. + * @param bcp47Locale - The locale of the (first) keyphrase associated with this model. + * @return - {@code true} if successful, {@code false} if unsuccessful + */ + KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, int userHandle, + String bcp47Locale); + + //TODO(273286174): We only support one keyphrase currently. + /** + * Returns the first matching {@link KeyphraseSoundModel} for the keyphrase ID, locale pair, + * contingent on the userHandle existing in the user list for the model. + * Returns null if a match isn't found. + * + * @param keyphrase - The text of (the first) keyphrase of the KeyphraseSoundModel to query. + * @param userHandle - The user handle making this request. Must be included in the user + * list of the registered sound model. + * @param bcp47Locale - The locale of the (first) keyphrase associated with this model. + * @return - {@code true} if successful, {@code false} if unsuccessful + */ + KeyphraseSoundModel getKeyphraseSoundModel(String keyphrase, int userHandle, + String bcp47Locale); + + /** + * Dumps contents of database for dumpsys + */ + void dump(PrintWriter pw); +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/TestModelEnrollmentDatabase.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/TestModelEnrollmentDatabase.java new file mode 100644 index 000000000000..9bbaf8e25f95 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/TestModelEnrollmentDatabase.java @@ -0,0 +1,148 @@ +/** + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.voiceinteraction; + +import android.annotation.NonNull; +import android.hardware.soundtrigger.SoundTrigger.Keyphrase; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.StringJoiner; + +/** + * In memory model enrollment database for testing purposes. + * @hide + */ +public class TestModelEnrollmentDatabase implements IEnrolledModelDb { + + // Record representing the primary key used in the real model database. + private static final class EnrollmentKey { + private final int mKeyphraseId; + private final List<Integer> mUserIds; + private final String mLocale; + + EnrollmentKey(int keyphraseId, + @NonNull List<Integer> userIds, @NonNull String locale) { + mKeyphraseId = keyphraseId; + mUserIds = Objects.requireNonNull(userIds); + mLocale = Objects.requireNonNull(locale); + } + + int keyphraseId() { + return mKeyphraseId; + } + + List<Integer> userIds() { + return mUserIds; + } + + String locale() { + return mLocale; + } + + @Override + public String toString() { + StringJoiner sj = new StringJoiner(", ", "{", "}"); + sj.add("keyphraseId: " + mKeyphraseId); + sj.add("userIds: " + mUserIds.toString()); + sj.add("locale: " + mLocale.toString()); + return "EnrollmentKey: " + sj.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int res = 1; + res = prime * res + mKeyphraseId; + res = prime * res + mUserIds.hashCode(); + res = prime * res + mLocale.hashCode(); + return res; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null) return false; + if (!(other instanceof EnrollmentKey)) return false; + EnrollmentKey that = (EnrollmentKey) other; + if (mKeyphraseId != that.mKeyphraseId) return false; + if (!mUserIds.equals(that.mUserIds)) return false; + if (!mLocale.equals(that.mLocale)) return false; + return true; + } + + } + + private final Map<EnrollmentKey, KeyphraseSoundModel> mModelMap = new HashMap<>(); + + @Override + public boolean updateKeyphraseSoundModel(KeyphraseSoundModel soundModel) { + final Keyphrase keyphrase = soundModel.getKeyphrases()[0]; + mModelMap.put(new EnrollmentKey(keyphrase.getId(), + Arrays.stream(keyphrase.getUsers()).boxed().toList(), + keyphrase.getLocale().toLanguageTag()), + soundModel); + return true; + } + + @Override + public boolean deleteKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale) { + return mModelMap.keySet().removeIf(key -> (key.keyphraseId() == keyphraseId) + && key.locale().equals(bcp47Locale) + && key.userIds().contains(userHandle)); + } + + @Override + public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, int userHandle, + String bcp47Locale) { + return mModelMap.entrySet() + .stream() + .filter((entry) -> (entry.getKey().keyphraseId() == keyphraseId) + && entry.getKey().locale().equals(bcp47Locale) + && entry.getKey().userIds().contains(userHandle)) + .findFirst() + .map((entry) -> entry.getValue()) + .orElse(null); + } + + @Override + public KeyphraseSoundModel getKeyphraseSoundModel(String keyphrase, int userHandle, + String bcp47Locale) { + return mModelMap.entrySet() + .stream() + .filter((entry) -> (entry.getValue().getKeyphrases()[0].getText().equals(keyphrase) + && entry.getKey().locale().equals(bcp47Locale) + && entry.getKey().userIds().contains(userHandle))) + .findFirst() + .map((entry) -> entry.getValue()) + .orElse(null); + } + + + /** + * Dumps contents of database for dumpsys + */ + public void dump(PrintWriter pw) { + pw.println("Using test enrollment database, with enrolled models:"); + pw.println(mModelMap); + } +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index e1da2ca2a086..1d7b966bab51 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -99,11 +99,11 @@ import com.android.internal.os.BackgroundThread; import com.android.internal.util.DumpUtils; import com.android.server.FgThread; import com.android.server.LocalServices; +import com.android.server.SoundTriggerInternal; import com.android.server.SystemService; import com.android.server.UiThread; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.permission.LegacyPermissionManagerInternal; -import com.android.server.soundtrigger.SoundTriggerInternal; import com.android.server.utils.Slogf; import com.android.server.utils.TimingsTraceAndSlog; import com.android.server.wm.ActivityTaskManagerInternal; @@ -125,7 +125,9 @@ public class VoiceInteractionManagerService extends SystemService { final Context mContext; final ContentResolver mResolver; - final DatabaseHelper mDbHelper; + // Can be overridden for testing purposes + private IEnrolledModelDb mDbHelper; + private final IEnrolledModelDb mRealDbHelper; final ActivityManagerInternal mAmInternal; final ActivityTaskManagerInternal mAtmInternal; final UserManagerInternal mUserManagerInternal; @@ -143,7 +145,7 @@ public class VoiceInteractionManagerService extends SystemService { mResolver = context.getContentResolver(); mUserManagerInternal = Objects.requireNonNull( LocalServices.getService(UserManagerInternal.class)); - mDbHelper = new DatabaseHelper(context); + mDbHelper = mRealDbHelper = new DatabaseHelper(context); mServiceStub = new VoiceInteractionManagerServiceStub(); mAmInternal = Objects.requireNonNull( LocalServices.getService(ActivityManagerInternal.class)); @@ -1605,6 +1607,42 @@ public class VoiceInteractionManagerService extends SystemService { } } + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_VOICE_KEYPHRASES) + public void setModelDatabaseForTestEnabled(boolean enabled, IBinder token) { + super.setModelDatabaseForTestEnabled_enforcePermission(); + enforceCallerAllowedToEnrollVoiceModel(); + synchronized (this) { + if (enabled) { + // Replace the dbhelper with a new test db + final var db = new TestModelEnrollmentDatabase(); + try { + // Listen to our caller death, and make sure we revert to the real + // db if they left the model in a test state. + token.linkToDeath(() -> { + synchronized (this) { + if (mDbHelper == db) { + mDbHelper = mRealDbHelper; + mImpl.notifySoundModelsChangedLocked(); + } + } + }, 0); + } catch (RemoteException e) { + // If the caller is already dead, nothing to do. + return; + } + mDbHelper = db; + mImpl.notifySoundModelsChangedLocked(); + } else { + // Nothing to do if the db is already set to the real impl. + if (mDbHelper != mRealDbHelper) { + mDbHelper = mRealDbHelper; + mImpl.notifySoundModelsChangedLocked(); + } + } + } + } + //----------------- SoundTrigger APIs --------------------------------// @Override public boolean isEnrolledForKeyphrase(int keyphraseId, String bcp47Locale) { @@ -1712,28 +1750,27 @@ public class VoiceInteractionManagerService extends SystemService { final long caller = Binder.clearCallingIdentity(); try { KeyphraseSoundModel soundModel = - mDbHelper.getKeyphraseSoundModel(keyphraseId, callingUserId, bcp47Locale); + mDbHelper.getKeyphraseSoundModel(keyphraseId, + callingUserId, bcp47Locale); if (soundModel == null || soundModel.getUuid() == null || soundModel.getKeyphrases() == null) { Slog.w(TAG, "No matching sound model found in startRecognition"); return SoundTriggerInternal.STATUS_ERROR; - } else { - // Regardless of the status of the start recognition, we need to make sure - // that we unload this model if needed later. - synchronized (VoiceInteractionManagerServiceStub.this) { - mLoadedKeyphraseIds.put(keyphraseId, this); - if (mSessionExternalCallback == null - || mSessionInternalCallback == null - || callback.asBinder() != mSessionExternalCallback.asBinder()) { - mSessionInternalCallback = createSoundTriggerCallbackLocked( - callback); - mSessionExternalCallback = callback; - } + } + // Regardless of the status of the start recognition, we need to make sure + // that we unload this model if needed later. + synchronized (VoiceInteractionManagerServiceStub.this) { + mLoadedKeyphraseIds.put(keyphraseId, this); + if (mSessionExternalCallback == null + || mSessionInternalCallback == null + || callback.asBinder() != mSessionExternalCallback.asBinder()) { + mSessionInternalCallback = createSoundTriggerCallbackLocked(callback); + mSessionExternalCallback = callback; } - return mSession.startRecognition(keyphraseId, soundModel, - mSessionInternalCallback, recognitionConfig, runInBatterySaverMode); } + return mSession.startRecognition(keyphraseId, soundModel, + mSessionInternalCallback, recognitionConfig, runInBatterySaverMode); } finally { Binder.restoreCallingIdentity(caller); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index 62be2a555bc4..0ad86c11d29a 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -71,7 +71,6 @@ import android.util.PrintWriterPrinter; import android.util.Slog; import android.view.IWindowManager; -import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IVisualQueryDetectionAttentionListener; import com.android.internal.app.IVoiceActionCheckCallback; @@ -248,7 +247,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne Context.RECEIVER_EXPORTED); } - @GuardedBy("this") public void grantImplicitAccessLocked(int grantRecipientUid, @Nullable Intent intent) { final int grantRecipientAppId = UserHandle.getAppId(grantRecipientUid); final int grantRecipientUserId = UserHandle.getUserId(grantRecipientUid); @@ -258,7 +256,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne /* direct= */ true); } - @GuardedBy("this") public boolean showSessionLocked(@Nullable Bundle args, int flags, @Nullable String attributionTag, @Nullable IVoiceInteractionSessionShowCallback showCallback, @@ -331,7 +328,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") public boolean hideSessionLocked() { if (mActiveSession != null) { return mActiveSession.hideLocked(); @@ -339,7 +335,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne return false; } - @GuardedBy("this") public boolean deliverNewSessionLocked(IBinder token, IVoiceInteractionSession session, IVoiceInteractor interactor) { if (mActiveSession == null || token != mActiveSession.mToken) { @@ -350,7 +345,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne return true; } - @GuardedBy("this") public int startVoiceActivityLocked(@Nullable String callingFeatureId, int callingPid, int callingUid, IBinder token, Intent intent, String resolvedType) { try { @@ -373,7 +367,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") public int startAssistantActivityLocked(@Nullable String callingFeatureId, int callingPid, int callingUid, IBinder token, Intent intent, String resolvedType, @NonNull Bundle bundle) { @@ -397,7 +390,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") public void requestDirectActionsLocked(@NonNull IBinder token, int taskId, @NonNull IBinder assistToken, @Nullable RemoteCallback cancellationCallback, @NonNull RemoteCallback callback) { @@ -453,7 +445,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") void performDirectActionLocked(@NonNull IBinder token, @NonNull String actionId, @Nullable Bundle arguments, int taskId, IBinder assistToken, @Nullable RemoteCallback cancellationCallback, @@ -480,7 +471,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") public void setKeepAwakeLocked(IBinder token, boolean keepAwake) { try { if (mActiveSession == null || token != mActiveSession.mToken) { @@ -493,7 +483,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") public void closeSystemDialogsLocked(IBinder token) { try { if (mActiveSession == null || token != mActiveSession.mToken) { @@ -506,7 +495,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") public void finishLocked(IBinder token, boolean finishTask) { if (mActiveSession == null || (!finishTask && token != mActiveSession.mToken)) { Slog.w(TAG, "finish does not match active session"); @@ -516,7 +504,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mActiveSession = null; } - @GuardedBy("this") public void setDisabledShowContextLocked(int callingUid, int flags) { int activeUid = mInfo.getServiceInfo().applicationInfo.uid; if (callingUid != activeUid) { @@ -526,7 +513,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mDisabledShowContext = flags; } - @GuardedBy("this") public int getDisabledShowContextLocked(int callingUid) { int activeUid = mInfo.getServiceInfo().applicationInfo.uid; if (callingUid != activeUid) { @@ -536,7 +522,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne return mDisabledShowContext; } - @GuardedBy("this") public int getUserDisabledShowContextLocked(int callingUid) { int activeUid = mInfo.getServiceInfo().applicationInfo.uid; if (callingUid != activeUid) { @@ -550,7 +535,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne return mInfo.getSupportsLocalInteraction(); } - @GuardedBy("this") public void startListeningVisibleActivityChangedLocked(@NonNull IBinder token) { if (DEBUG) { Slog.d(TAG, "startListeningVisibleActivityChangedLocked: token=" + token); @@ -563,7 +547,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mActiveSession.startListeningVisibleActivityChangedLocked(); } - @GuardedBy("this") public void stopListeningVisibleActivityChangedLocked(@NonNull IBinder token) { if (DEBUG) { Slog.d(TAG, "stopListeningVisibleActivityChangedLocked: token=" + token); @@ -576,7 +559,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mActiveSession.stopListeningVisibleActivityChangedLocked(); } - @GuardedBy("this") public void notifyActivityDestroyedLocked(@NonNull IBinder activityToken) { if (DEBUG) { Slog.d(TAG, "notifyActivityDestroyedLocked activityToken=" + activityToken); @@ -591,7 +573,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mActiveSession.notifyActivityDestroyedLocked(activityToken); } - @GuardedBy("this") public void notifyActivityEventChangedLocked(@NonNull IBinder activityToken, int type) { if (DEBUG) { Slog.d(TAG, "notifyActivityEventChangedLocked type=" + type); @@ -606,7 +587,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mActiveSession.notifyActivityEventChangedLocked(activityToken, type); } - @GuardedBy("this") public void updateStateLocked( @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, @@ -627,7 +607,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") private void verifyDetectorForHotwordDetectionLocked( @Nullable SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback, @@ -685,7 +664,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne voiceInteractionServiceUid); } - @GuardedBy("this") private void verifyDetectorForVisualQueryDetectionLocked(@Nullable SharedMemory sharedMemory) { Slog.v(TAG, "verifyDetectorForVisualQueryDetectionLocked"); @@ -724,7 +702,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") public void initAndVerifyDetectorLocked( @NonNull Identity voiceInteractorIdentity, @Nullable PersistableBundle options, @@ -769,7 +746,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne detectorType); } - @GuardedBy("this") public void destroyDetectorLocked(IBinder token) { Slog.v(TAG, "destroyDetectorLocked"); @@ -788,7 +764,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") public void shutdownHotwordDetectionServiceLocked() { if (DEBUG) { Slog.d(TAG, "shutdownHotwordDetectionServiceLocked"); @@ -801,7 +776,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection = null; } - @GuardedBy("this") public void setVisualQueryDetectionAttentionListenerLocked( @Nullable IVisualQueryDetectionAttentionListener listener) { if (mHotwordDetectionConnection == null) { @@ -810,7 +784,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.setVisualQueryDetectionAttentionListenerLocked(listener); } - @GuardedBy("this") public void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) { if (DEBUG) { Slog.d(TAG, "startPerceivingLocked"); @@ -824,7 +797,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.startPerceivingLocked(callback); } - @GuardedBy("this") public void stopPerceivingLocked() { if (DEBUG) { Slog.d(TAG, "stopPerceivingLocked"); @@ -838,7 +810,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.stopPerceivingLocked(); } - @GuardedBy("this") public void startListeningFromMicLocked( AudioFormat audioFormat, IMicrophoneHotwordDetectionVoiceInteractionCallback callback) { @@ -854,7 +825,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.startListeningFromMicLocked(audioFormat, callback); } - @GuardedBy("this") public void startListeningFromExternalSourceLocked( ParcelFileDescriptor audioStream, AudioFormat audioFormat, @@ -879,7 +849,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne options, token, callback); } - @GuardedBy("this") public void stopListeningFromMicLocked() { if (DEBUG) { Slog.d(TAG, "stopListeningFromMicLocked"); @@ -893,7 +862,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.stopListeningFromMicLocked(); } - @GuardedBy("this") public void triggerHardwareRecognitionEventForTestLocked( SoundTrigger.KeyphraseRecognitionEvent event, IHotwordRecognitionStatusCallback callback) { @@ -908,7 +876,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.triggerHardwareRecognitionEventForTestLocked(event, callback); } - @GuardedBy("this") public IRecognitionStatusCallback createSoundTriggerCallbackLocked( IHotwordRecognitionStatusCallback callback) { if (DEBUG) { @@ -933,12 +900,11 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne return null; } - @GuardedBy("this") boolean isIsolatedProcessLocked(@NonNull ServiceInfo serviceInfo) { return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0 && (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0; } - @GuardedBy("this") + boolean verifyProcessSharingLocked() { // only check this if both VQDS and HDS are declared in the app ServiceInfo hotwordInfo = getServiceInfoLocked(mHotwordDetectionComponentName, mUser); @@ -960,7 +926,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.forceRestart(); } - @GuardedBy("this") void setDebugHotwordLoggingLocked(boolean logging) { if (mHotwordDetectionConnection == null) { Slog.w(TAG, "Failed to set temporary debug logging: no hotword detection active"); @@ -969,7 +934,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.setDebugHotwordLoggingLocked(logging); } - @GuardedBy("this") void resetHotwordDetectionConnectionLocked() { if (DEBUG) { Slog.d(TAG, "resetHotwordDetectionConnectionLocked"); @@ -984,7 +948,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection = null; } - @GuardedBy("this") public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args) { if (!mValid) { pw.print(" NOT VALID: "); @@ -1023,7 +986,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") void startLocked() { Intent intent = new Intent(VoiceInteractionService.SERVICE_INTERFACE); intent.setComponent(mComponent); @@ -1048,7 +1010,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") void shutdownLocked() { // If there is an active session, cancel it to allow it to clean up its window and other // state. @@ -1076,7 +1037,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } - @GuardedBy("this") void notifySoundModelsChangedLocked() { if (mService == null) { Slog.w(TAG, "Not bound to voice interaction service " + mComponent); diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index e39af5aa3327..9dd2a61671ec 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -2682,71 +2682,76 @@ public class TelecomManager { } /** - * Reports a new call with the specified {@link CallAttributes} to the telecom service. This - * method can be used to report both incoming and outgoing calls. By reporting the call, the - * system is aware of the call and can provide updates on services (ex. Another device wants to - * disconnect the call) or events (ex. a new Bluetooth route became available). - * + * Add a call to the Android system service Telecom. This allows the system to start tracking an + * incoming or outgoing call with the specified {@link CallAttributes}. Once the call is ready + * to be disconnected, use the {@link CallControl#disconnect(DisconnectCause, Executor, + * OutcomeReceiver)} which is provided by the {@code pendingControl#onResult(CallControl)}. * <p> - * The difference between this API call and {@link TelecomManager#placeCall(Uri, Bundle)} or - * {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)} is that this API - * will asynchronously provide an update on whether the new call was added successfully via - * an {@link OutcomeReceiver}. Additionally, callbacks will run on the executor thread that was - * passed in. - * * <p> - * Note: Only packages that register with + * <p> + * <b>Call Lifecycle</b>: Your app is given foreground execution priority as long as you have a + * valid call and are posting a {@link android.app.Notification.CallStyle} notification. + * When your application is given foreground execution priority, your app is treated as a + * foreground service. Foreground execution priority will prevent the + * {@link android.app.ActivityManager} from killing your application when it is placed the + * background. Foreground execution priority is removed from your app when all of your app's + * calls terminate or your app no longer posts a valid notification. + * <p> + * <p> + * <p> + * <b>Note</b>: Only packages that register with * {@link PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS} * can utilize this API. {@link PhoneAccount}s that set the capabilities * {@link PhoneAccount#CAPABILITY_SIM_SUBSCRIPTION}, * {@link PhoneAccount#CAPABILITY_CALL_PROVIDER}, * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} * are not supported and will cause an exception to be thrown. - * * <p> - * Usage example: + * <p> + * <p> + * <b>Usage example:</b> * <pre> - * - * // An app should first define their own construct of a Call that overrides all the - * // {@link CallControlCallback}s and {@link CallEventCallback}s - * private class MyVoipCall { - * public String callId = ""; - * - * public CallControlCallEventCallback handshakes = new - * CallControlCallEventCallback() { - * // override/ implement all {@link CallControlCallback}s + * // Its up to your app on how you want to wrap the objects. One such implementation can be: + * class MyVoipCall { + * ... + * public CallControlCallEventCallback handshakes = new CallControlCallback() { + * ... * } - * public CallEventCallback events = new - * CallEventCallback() { - * // override/ implement all {@link CallEventCallback}s - * } - * public MyVoipCall(String id){ - * callId = id; - * } * - * PhoneAccountHandle handle = new PhoneAccountHandle( - * new ComponentName("com.example.voip.app", - * "com.example.voip.app.NewCallActivity"), "123"); + * public CallEventCallback events = new CallEventCallback() { + * ... + * } * - * CallAttributes callAttributes = new CallAttributes.Builder(handle, - * CallAttributes.DIRECTION_OUTGOING, - * "John Smith", Uri.fromParts("tel", "123", null)) - * .build(); + * public MyVoipCall(String id){ + * ... + * } + * } * * MyVoipCall myFirstOutgoingCall = new MyVoipCall("1"); * - * telecomManager.addCall(callAttributes, Runnable::run, new OutcomeReceiver() { + * telecomManager.addCall(callAttributes, + * Runnable::run, + * new OutcomeReceiver() { * public void onResult(CallControl callControl) { - * // The call has been added successfully + * // The call has been added successfully. For demonstration + * // purposes, the call is disconnected immediately ... + * callControl.disconnect( + * new DisconnectCause(DisconnectCause.LOCAL) ) * } - * }, myFirstOutgoingCall.handshakes, myFirstOutgoingCall.events); + * }, + * myFirstOutgoingCall.handshakes, + * myFirstOutgoingCall.events); * </pre> * - * @param callAttributes attributes of the new call (incoming or outgoing, address, etc. ) - * @param executor thread to run background CallEventCallback updates on - * @param pendingControl OutcomeReceiver that receives the result of addCall transaction - * @param handshakes object that overrides {@link CallControlCallback}s - * @param events object that overrides {@link CallEventCallback}s + * @param callAttributes attributes of the new call (incoming or outgoing, address, etc.) + * @param executor execution context to run {@link CallControlCallback} updates on + * @param pendingControl Receives the result of addCall transaction. Upon success, a + * CallControl object is provided which can be used to do things like + * disconnect the call that was added. + * @param handshakes callback that receives <b>actionable</b> updates that originate from + * Telecom. + * @param events callback that receives <b>non</b>-actionable updates that originate + * from Telecom. */ @RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS) @SuppressLint("SamShouldBeLast") diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index cbdf38ae95d4..ee9d6c1a2448 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2989,4 +2989,14 @@ interface ITelephony { * {@code false} otherwise. */ boolean setSatelliteServicePackageName(in String servicePackageName); + + /** + * This API can be used by only CTS to update the timeout duration in milliseconds that + * satellite should stay at listening mode to wait for the next incoming page before disabling + * listening mode. + * + * @param timeoutMillis The timeout duration in millisecond. + * @return {@code true} if the timeout duration is set successfully, {@code false} otherwise. + */ + boolean setSatelliteListeningTimeoutDuration(in long timeoutMillis); } diff --git a/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml b/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml index d2653d0de0d4..f20dd424c617 100644 --- a/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml +++ b/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml @@ -18,6 +18,6 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android"> <gradient android:startColor="#000000" - android:endColor="#181818" + android:endColor="#222222" android:angle="0"/> </shape>
\ No newline at end of file diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/NetworkProviderInfo.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/NetworkProviderInfo.java index 25fbabce71ae..166fbddc2f56 100644 --- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/NetworkProviderInfo.java +++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/NetworkProviderInfo.java @@ -163,11 +163,11 @@ public final class NetworkProviderInfo implements Parcelable { /** * Sets the displayed connection strength of the remote device to the internet. * - * @param connectionStrength Connection strength in range 0 to 3. + * @param connectionStrength Connection strength in range 0 to 4. * @return Returns the Builder object. */ @NonNull - public Builder setConnectionStrength(@IntRange(from = 0, to = 3) int connectionStrength) { + public Builder setConnectionStrength(@IntRange(from = 0, to = 4) int connectionStrength) { mConnectionStrength = connectionStrength; return this; } @@ -205,8 +205,8 @@ public final class NetworkProviderInfo implements Parcelable { if (batteryPercentage < 0 || batteryPercentage > 100) { throw new IllegalArgumentException("BatteryPercentage must be in range 0-100"); } - if (connectionStrength < 0 || connectionStrength > 3) { - throw new IllegalArgumentException("ConnectionStrength must be in range 0-3"); + if (connectionStrength < 0 || connectionStrength > 4) { + throw new IllegalArgumentException("ConnectionStrength must be in range 0-4"); } } @@ -265,9 +265,9 @@ public final class NetworkProviderInfo implements Parcelable { /** * Gets the displayed connection strength of the remote device to the internet. * - * @return Returns the connection strength in range 0 to 3. + * @return Returns the connection strength in range 0 to 4. */ - @IntRange(from = 0, to = 3) + @IntRange(from = 0, to = 4) public int getConnectionStrength() { return mConnectionStrength; } diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java index e5ef62b16dfd..feef0497c152 100644 --- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java +++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java @@ -35,6 +35,7 @@ import android.os.Binder; import android.os.IBinder; import android.os.IInterface; import android.os.RemoteException; +import android.text.TextUtils; import android.util.Log; import com.android.internal.R; @@ -173,10 +174,15 @@ public class SharedConnectivityManager { R.string.config_sharedConnectivityServicePackage); String serviceIntentAction = resources.getString( R.string.config_sharedConnectivityServiceIntentAction); + if (TextUtils.isEmpty(servicePackageName) || TextUtils.isEmpty(serviceIntentAction)) { + Log.e(TAG, "To support shared connectivity service on this device, the" + + " service's package name and intent action strings must not be empty"); + return null; + } return new SharedConnectivityManager(context, servicePackageName, serviceIntentAction); } catch (Resources.NotFoundException e) { Log.e(TAG, "To support shared connectivity service on this device, the service's" - + " package name and intent action string must be defined"); + + " package name and intent action strings must be defined"); } return null; } diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java index b585bd5cfd7b..a03a6c2fc673 100644 --- a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java +++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java @@ -105,6 +105,13 @@ public class SharedConnectivityManagerTest { } @Test + public void resourceStringsAreEmpty_createShouldReturnNull() { + when(mResources.getString(anyInt())).thenReturn(""); + + assertThat(SharedConnectivityManager.create(mContext)).isNull(); + } + + @Test public void bindingToServiceOnFirstCallbackRegistration() { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.registerCallback(mExecutor, mClientCallback); |