diff options
187 files changed, 5254 insertions, 1425 deletions
diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp index 8b55e071e715..558629537253 100644 --- a/apex/jobscheduler/service/Android.bp +++ b/apex/jobscheduler/service/Android.bp @@ -28,6 +28,7 @@ java_library { static_libs: [ "modules-utils-fastxmlserializer", + "service-jobscheduler-alarm.flags-aconfig-java", "service-jobscheduler-job.flags-aconfig-java", ], diff --git a/apex/jobscheduler/service/aconfig/Android.bp b/apex/jobscheduler/service/aconfig/Android.bp index 3ba7aa201d27..4db39dc1976b 100644 --- a/apex/jobscheduler/service/aconfig/Android.bp +++ b/apex/jobscheduler/service/aconfig/Android.bp @@ -27,3 +27,15 @@ java_aconfig_library { aconfig_declarations: "service-job.flags-aconfig", visibility: ["//frameworks/base:__subpackages__"], } + +// Alarm +aconfig_declarations { + name: "alarm_flags", + package: "com.android.server.alarm", + srcs: ["alarm.aconfig"], +} + +java_aconfig_library { + name: "service-jobscheduler-alarm.flags-aconfig-java", + aconfig_declarations: "alarm_flags", +} diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig new file mode 100644 index 000000000000..3b9b4e70b310 --- /dev/null +++ b/apex/jobscheduler/service/aconfig/alarm.aconfig @@ -0,0 +1,11 @@ +package: "com.android.server.alarm" + +flag { + name: "use_frozen_state_to_drop_listener_alarms" + namespace: "backstage_power" + description: "Use frozen state callback to drop listener alarms for cached apps" + bug: "324470945" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 39de0af3c8d0..e728a2c55765 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -16,6 +16,7 @@ package com.android.server.alarm; +import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.AlarmManager.ELAPSED_REALTIME; import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; @@ -75,6 +76,7 @@ import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.Activity; +import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityOptions; import android.app.AlarmManager; @@ -103,6 +105,7 @@ import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.Message; @@ -145,6 +148,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.LocalLog; @@ -293,6 +297,7 @@ public class AlarmManagerService extends SystemService { private final Injector mInjector; int mBroadcastRefCount = 0; + boolean mUseFrozenStateToDropListenerAlarms; MetricsHelper mMetricsHelper; PowerManager.WakeLock mWakeLock; SparseIntArray mAlarmsPerUid = new SparseIntArray(); @@ -1856,15 +1861,47 @@ public class AlarmManagerService extends SystemService { @Override public void onStart() { mInjector.init(); + mHandler = new AlarmHandler(); + mOptsWithFgs.setPendingIntentBackgroundActivityLaunchAllowed(false); mOptsWithFgsForAlarmClock.setPendingIntentBackgroundActivityLaunchAllowed(false); mOptsWithoutFgs.setPendingIntentBackgroundActivityLaunchAllowed(false); mOptsTimeBroadcast.setPendingIntentBackgroundActivityLaunchAllowed(false); mActivityOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false); mBroadcastOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false); + mMetricsHelper = new MetricsHelper(getContext(), mLock); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms(); + if (mUseFrozenStateToDropListenerAlarms) { + final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> { + final int size = frozenStates.length; + if (uids.length != size) { + Slog.wtf(TAG, "Got different length arrays in frozen state callback!" + + " uids.length: " + uids.length + " frozenStates.length: " + size); + // Cannot process received data in any meaningful way. + return; + } + final IntArray affectedUids = new IntArray(); + for (int i = 0; i < size; i++) { + if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) { + continue; + } + if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, + uids[i])) { + continue; + } + affectedUids.add(uids[i]); + } + if (affectedUids.size() > 0) { + removeExactListenerAlarms(affectedUids.toArray()); + } + }; + final ActivityManager am = getContext().getSystemService(ActivityManager.class); + am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback); + } + mListenerDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { @@ -1880,7 +1917,6 @@ public class AlarmManagerService extends SystemService { }; synchronized (mLock) { - mHandler = new AlarmHandler(); mConstants = new Constants(mHandler); mAlarmStore = new LazyAlarmStore(); @@ -1960,6 +1996,21 @@ public class AlarmManagerService extends SystemService { publishBinderService(Context.ALARM_SERVICE, mService); } + private void removeExactListenerAlarms(int... whichUids) { + synchronized (mLock) { + removeAlarmsInternalLocked(a -> { + if (!ArrayUtils.contains(whichUids, a.uid) || a.listener == null + || a.windowLength != 0) { + return false; + } + Slog.w(TAG, "Alarm " + a.listenerTag + " being removed for " + + UserHandle.formatUid(a.uid) + ":" + a.packageName + + " because the app got frozen"); + return true; + }, REMOVE_REASON_LISTENER_CACHED); + } + } + void refreshExactAlarmCandidates() { final String[] candidates = mLocalPermissionManager.getAppOpPermissionPackages( Manifest.permission.SCHEDULE_EXACT_ALARM); @@ -3074,6 +3125,14 @@ public class AlarmManagerService extends SystemService { mConstants.dump(pw); pw.println(); + pw.println("Feature Flags:"); + pw.increaseIndent(); + pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS, + mUseFrozenStateToDropListenerAlarms); + pw.decreaseIndent(); + pw.println(); + pw.println(); + if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON) { pw.println("TARE details:"); pw.increaseIndent(); @@ -4959,18 +5018,7 @@ public class AlarmManagerService extends SystemService { break; case REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED: uid = (Integer) msg.obj; - synchronized (mLock) { - removeAlarmsInternalLocked(a -> { - if (a.uid != uid || a.listener == null || a.windowLength != 0) { - return false; - } - // TODO (b/265195908): Change to .w once we have some data on breakages. - Slog.wtf(TAG, "Alarm " + a.listenerTag + " being removed for " - + UserHandle.formatUid(a.uid) + ":" + a.packageName - + " because the app went into cached state"); - return true; - }, REMOVE_REASON_LISTENER_CACHED); - } + removeExactListenerAlarms(uid); break; default: // nope, just ignore it @@ -5322,6 +5370,10 @@ public class AlarmManagerService extends SystemService { @Override public void handleUidCachedChanged(int uid, boolean cached) { + if (mUseFrozenStateToDropListenerAlarms) { + // Use ActivityManager#UidFrozenStateChangedCallback instead. + return; + } if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, uid)) { return; } diff --git a/core/api/current.txt b/core/api/current.txt index e8eace1c51c8..3fd67db95e1b 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -56279,7 +56279,7 @@ package android.view.inputmethod { method public boolean acceptStylusHandwritingDelegation(@NonNull android.view.View); method public boolean acceptStylusHandwritingDelegation(@NonNull android.view.View, @NonNull String); method @FlaggedApi("android.view.inputmethod.use_zero_jank_proxy") public void acceptStylusHandwritingDelegation(@NonNull android.view.View, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); - method @FlaggedApi("android.view.inputmethod.home_screen_handwriting_delegator") public boolean acceptStylusHandwritingDelegation(@NonNull android.view.View, @NonNull String, int); + method @FlaggedApi("android.view.inputmethod.home_screen_handwriting_delegator") public void acceptStylusHandwritingDelegation(@NonNull android.view.View, @NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); method public void dispatchKeyEventFromInputMethod(@Nullable android.view.View, @NonNull android.view.KeyEvent); method public void displayCompletions(android.view.View, android.view.inputmethod.CompletionInfo[]); method @Nullable public android.view.inputmethod.InputMethodInfo getCurrentInputMethodInfo(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index cc7d97a6ee50..2ff1f766ad8f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -14204,7 +14204,8 @@ package android.telecom { method @Deprecated public java.util.List<android.telecom.PhoneAccountHandle> getPhoneAccountsForPackage(); method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public java.util.List<android.telecom.PhoneAccountHandle> getPhoneAccountsSupportingScheme(String); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean isInEmergencyCall(); - method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isInSelfManagedCall(@NonNull String, @NonNull android.os.UserHandle, boolean); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isInSelfManagedCall(@NonNull String, @NonNull android.os.UserHandle); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isInSelfManagedCall(@NonNull String, boolean); method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isRinging(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUserSelectedOutgoingPhoneAccount(@Nullable android.telecom.PhoneAccountHandle); field public static final String ACTION_CURRENT_TTY_MODE_CHANGED = "android.telecom.action.CURRENT_TTY_MODE_CHANGED"; diff --git a/core/java/Android.bp b/core/java/Android.bp index eba500dd32b4..34b8a878b3d1 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -126,6 +126,7 @@ filegroup { srcs: [ "android/os/IExternalVibrationController.aidl", "android/os/IExternalVibratorService.aidl", + "android/os/ExternalVibrationScale.aidl", ], } diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java index d139134c7c1a..fb1b17bd23d4 100644 --- a/core/java/android/app/assist/AssistStructure.java +++ b/core/java/android/app/assist/AssistStructure.java @@ -10,6 +10,7 @@ import android.annotation.SystemApi; import android.app.Activity; import android.content.ComponentName; import android.content.Context; +import android.credentials.CredentialOption; import android.credentials.GetCredentialException; import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; @@ -29,6 +30,7 @@ import android.os.PooledStringWriter; import android.os.RemoteException; import android.os.SystemClock; import android.service.autofill.FillRequest; +import android.service.credentials.CredentialProviderService; import android.text.InputType; import android.text.Spanned; import android.text.TextUtils; @@ -2258,6 +2260,17 @@ public class AssistStructure implements Parcelable { @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { mNode.mGetCredentialRequest = request; mNode.mGetCredentialCallback = callback; + for (CredentialOption option : request.getCredentialOptions()) { + ArrayList<AutofillId> ids = option.getCandidateQueryData() + .getParcelableArrayList( + CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class); + ids = ids != null ? ids : new ArrayList<>(); + if (!ids.contains(getAutofillId())) { + ids.add(getAutofillId()); + } + option.getCandidateQueryData() + .putParcelableArrayList(CredentialProviderService.EXTRA_AUTOFILL_ID, ids); + } } @Override diff --git a/core/java/android/os/ExternalVibrationScale.aidl b/core/java/android/os/ExternalVibrationScale.aidl new file mode 100644 index 000000000000..cf6f8ed52f7d --- /dev/null +++ b/core/java/android/os/ExternalVibrationScale.aidl @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2018, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +/** + * ExternalVibrationScale holds the vibration scale level and adaptive haptics scale. These + * can be used to scale external vibrations. + * + * @hide + */ +parcelable ExternalVibrationScale { + @Backing(type="int") + enum ScaleLevel { + SCALE_MUTE = -100, + SCALE_VERY_LOW = -2, + SCALE_LOW = -1, + SCALE_NONE = 0, + SCALE_HIGH = 1, + SCALE_VERY_HIGH = 2 + } + + /** + * The scale level that will be applied to external vibrations. + */ + ScaleLevel scaleLevel = ScaleLevel.SCALE_NONE; + + /** + * The adaptive haptics scale that will be applied to external vibrations. + */ + float adaptiveHapticsScale = 1f; +} diff --git a/core/java/android/os/IExternalVibratorService.aidl b/core/java/android/os/IExternalVibratorService.aidl index 666171fcbcb3..a9df15a088bf 100644 --- a/core/java/android/os/IExternalVibratorService.aidl +++ b/core/java/android/os/IExternalVibratorService.aidl @@ -17,6 +17,7 @@ package android.os; import android.os.ExternalVibration; +import android.os.ExternalVibrationScale; /** * The communication channel by which an external system that wants to control the system @@ -32,29 +33,24 @@ import android.os.ExternalVibration; * {@hide} */ interface IExternalVibratorService { - const int SCALE_MUTE = -100; - const int SCALE_VERY_LOW = -2; - const int SCALE_LOW = -1; - const int SCALE_NONE = 0; - const int SCALE_HIGH = 1; - const int SCALE_VERY_HIGH = 2; - /** * A method called by the external system to start a vibration. * - * If this returns {@code SCALE_MUTE}, then the vibration should <em>not</em> play. If this - * returns any other scale level, then any currently playing vibration controlled by the - * requesting system must be muted and this vibration can begin playback. + * This returns an {@link ExternalVibrationScale} which includes the vibration scale level and + * the adaptive haptics scale. + * + * If the returned scale level is {@link ExternalVibrationScale.ScaleLevel#SCALE_MUTE}, then + * the vibration should <em>not</em> play. If it returns any other scale level, then + * any currently playing vibration controlled by the requesting system must be muted and this + * vibration can begin playback. * * Note that the IExternalVibratorService implementation will not call mute on any currently * playing external vibrations in order to avoid re-entrancy with the system on the other side. * - * @param vibration An ExternalVibration - * - * @return {@code SCALE_MUTE} if the external vibration should not play, and any other scale - * level if it should. + * @param vib The external vibration starting. + * @return {@link ExternalVibrationScale} including scale level and adaptive haptics scale. */ - int onExternalVibrationStart(in ExternalVibration vib); + ExternalVibrationScale onExternalVibrationStart(in ExternalVibration vib); /** * A method called by the external system when a vibration no longer wants to play. diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ec4d5876070a..50adc40e719d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -12481,6 +12481,24 @@ public final class Settings { public static void setLocationProviderEnabled(ContentResolver cr, String provider, boolean enabled) { } + + /** + * List of system components that support restore in a V-> U OS downgrade but do not have + * RestoreAnyVersion set to true. Value set before system restore. + * This setting is not B&Rd + * List is stored as a comma-separated string of package names e.g. "a,b,c" + * @hide + */ + public static final String V_TO_U_RESTORE_ALLOWLIST = "v_to_u_restore_allowlist"; + + /** + * List of system components that have RestoreAnyVersion set to true but do not support + * restore in a V-> U OS downgrade. Value set before system restore. + * This setting is not B&Rd + * List is stored as a comma-separated string of package names e.g. "a,b,c" + * @hide + */ + public static final String V_TO_U_RESTORE_DENYLIST = "v_to_u_restore_denylist"; } /** diff --git a/core/java/android/view/BatchedInputEventReceiver.java b/core/java/android/view/BatchedInputEventReceiver.java index e679f2998ca1..ca2e56d383e5 100644 --- a/core/java/android/view/BatchedInputEventReceiver.java +++ b/core/java/android/view/BatchedInputEventReceiver.java @@ -19,6 +19,7 @@ package android.view; import android.compat.annotation.UnsupportedAppUsage; import android.os.Handler; import android.os.Looper; +import android.os.Trace; /** * Similar to {@link InputEventReceiver}, but batches events to vsync boundaries when possible. @@ -42,6 +43,8 @@ public class BatchedInputEventReceiver extends InputEventReceiver { super(inputChannel, looper); mChoreographer = choreographer; mBatchingEnabled = true; + traceBoolVariable("mBatchingEnabled", mBatchingEnabled); + traceBoolVariable("mBatchedInputScheduled", mBatchedInputScheduled); mHandler = new Handler(looper); } @@ -71,6 +74,7 @@ public class BatchedInputEventReceiver extends InputEventReceiver { } mBatchingEnabled = batchingEnabled; + traceBoolVariable("mBatchingEnabled", mBatchingEnabled); mHandler.removeCallbacks(mConsumeBatchedInputEvents); if (!batchingEnabled) { unscheduleBatchedInput(); @@ -81,6 +85,7 @@ public class BatchedInputEventReceiver extends InputEventReceiver { protected void doConsumeBatchedInput(long frameTimeNanos) { if (mBatchedInputScheduled) { mBatchedInputScheduled = false; + traceBoolVariable("mBatchedInputScheduled", mBatchedInputScheduled); if (consumeBatchedInputEvents(frameTimeNanos) && frameTimeNanos != -1) { // If we consumed a batch here, we want to go ahead and schedule the // consumption of batched input events on the next frame. Otherwise, we would @@ -95,6 +100,7 @@ public class BatchedInputEventReceiver extends InputEventReceiver { private void scheduleBatchedInput() { if (!mBatchedInputScheduled) { mBatchedInputScheduled = true; + traceBoolVariable("mBatchedInputScheduled", mBatchedInputScheduled); mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, mBatchedInputRunnable, null); } } @@ -102,11 +108,18 @@ public class BatchedInputEventReceiver extends InputEventReceiver { private void unscheduleBatchedInput() { if (mBatchedInputScheduled) { mBatchedInputScheduled = false; + traceBoolVariable("mBatchedInputScheduled", mBatchedInputScheduled); mChoreographer.removeCallbacks( Choreographer.CALLBACK_INPUT, mBatchedInputRunnable, null); } } + // @TODO(b/311142655): Delete this temporary tracing. It's only used here to debug a very + // specific issue. + private void traceBoolVariable(String name, boolean value) { + Trace.traceCounter(Trace.TRACE_TAG_INPUT, name, value ? 1 : 0); + } + private final class BatchedInputRunnable implements Runnable { @Override public void run() { diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 9c430cd4acb4..2cc05b0bc4b0 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -271,12 +271,29 @@ public abstract class InputEventReceiver { return mInputChannel.getToken(); } + private String getShortDescription(InputEvent event) { + if (event instanceof MotionEvent motion) { + return "MotionEvent " + MotionEvent.actionToString(motion.getAction()) + " deviceId=" + + motion.getDeviceId() + " source=0x" + + Integer.toHexString(motion.getSource()) + " historySize=" + + motion.getHistorySize(); + } else if (event instanceof KeyEvent key) { + return "KeyEvent " + KeyEvent.actionToString(key.getAction()) + + " deviceId=" + key.getDeviceId(); + } else { + Log.wtf(TAG, "Illegal InputEvent type: " + event); + return "InputEvent"; + } + } + // Called from native code. @SuppressWarnings("unused") @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void dispatchInputEvent(int seq, InputEvent event) { + Trace.traceBegin(Trace.TRACE_TAG_INPUT, "dispatchInputEvent " + getShortDescription(event)); mSeqMap.put(event.getSequenceNumber(), seq); onInputEvent(event); + Trace.traceEnd(Trace.TRACE_TAG_INPUT); } /** diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java index 021bbf7b9c9f..896b3f4bd5c3 100644 --- a/core/java/android/view/TextureView.java +++ b/core/java/android/view/TextureView.java @@ -196,8 +196,6 @@ public class TextureView extends View { private Canvas mCanvas; private int mSaveCount; - @Surface.FrameRateCompatibility int mFrameRateCompatibility; - private final Object[] mNativeWindowLock = new Object[0]; // Set by native code, do not write! @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 028448ccf871..596c52dcfdf6 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -24,6 +24,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; +import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW; @@ -85,6 +86,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.credentials.CredentialManager; +import android.credentials.CredentialOption; import android.credentials.GetCredentialException; import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; @@ -129,6 +131,7 @@ import android.os.SystemClock; import android.os.Trace; import android.os.Vibrator; import android.os.vibrator.Flags; +import android.service.credentials.CredentialProviderService; import android.sysprop.DisplayProperties; import android.text.InputType; import android.text.TextUtils; @@ -2366,6 +2369,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static boolean sToolkitSetFrameRateReadOnlyFlagValue; private static boolean sToolkitMetricsForFrameRateDecisionFlagValue; + // Used to set frame rate compatibility. + @Surface.FrameRateCompatibility int mFrameRateCompatibility = + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; static { EMPTY_STATE_SET = StateSet.get(0); @@ -7033,6 +7039,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { Preconditions.checkNotNull(request, "request must not be null"); Preconditions.checkNotNull(callback, "request must not be null"); + + for (CredentialOption option : request.getCredentialOptions()) { + ArrayList<AutofillId> ids = option.getCandidateQueryData() + .getParcelableArrayList( + CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class); + ids = ids != null ? ids : new ArrayList<>(); + if (!ids.contains(getAutofillId())) { + ids.add(getAutofillId()); + } + option.getCandidateQueryData() + .putParcelableArrayList(CredentialProviderService.EXTRA_AUTOFILL_ID, ids); + } mViewCredentialHandler = new ViewCredentialHandler(request, callback); } @@ -10875,8 +10893,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, structure.setAutofillId(new AutofillId(getAutofillId(), AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId()))); } - structure.setCredentialManagerRequest(getCredentialManagerRequest(), - getCredentialManagerCallback()); + if (getViewCredentialHandler() != null) { + structure.setCredentialManagerRequest( + getViewCredentialHandler().getRequest(), + getViewCredentialHandler().getCallback()); + } CharSequence cname = info.getClassName(); structure.setClassName(cname != null ? cname.toString() : null); structure.setContentDescription(info.getContentDescription()); @@ -33524,7 +33545,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, frameRateCateogry = FRAME_RATE_CATEGORY_HIGH; } } else { - viewRootImpl.votePreferredFrameRate(mPreferredFrameRate); + viewRootImpl.votePreferredFrameRate(mPreferredFrameRate, + mFrameRateCompatibility); return; } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 1e79786d7554..94260b223dd2 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -30,6 +30,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; +import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; @@ -1027,6 +1028,13 @@ public final class ViewRootImpl implements ViewParent, // as needed and can be useful for power saving. // Should not enable the dVRR feature if the value is false. private boolean mIsFrameRatePowerSavingsBalanced = true; + // Used to check if there is a conflict between different frame rate voting. + // Take 24 and 30 as an example, 24 is not a divisor of 30. + // We consider there is a conflict. + private boolean mIsFrameRateConflicted = false; + // Used to set frame rate compatibility. + @Surface.FrameRateCompatibility int mFrameRateCompatibility = + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; // time for touch boost period. private static final int FRAME_RATE_TOUCH_BOOST_TIME = 3000; // time for checking idle status periodically. @@ -4110,6 +4118,7 @@ public final class ViewRootImpl implements ViewParent, ? mFrameRateCategoryLowCount - 1 : mFrameRateCategoryLowCount; mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; mPreferredFrameRate = -1; + mIsFrameRateConflicted = false; } private void createSyncIfNeeded() { @@ -6508,6 +6517,7 @@ public final class ViewRootImpl implements ViewParent, break; case MSG_FRAME_RATE_SETTING: mPreferredFrameRate = 0; + mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; setPreferredFrameRate(mPreferredFrameRate); break; } @@ -12286,9 +12296,15 @@ public final class ViewRootImpl implements ViewParent, if (!shouldSetFrameRateCategory()) { return; } + int categoryFromConflictedFrameRates = FRAME_RATE_CATEGORY_NO_PREFERENCE; + if (mIsFrameRateConflicted) { + categoryFromConflictedFrameRates = mPreferredFrameRate > 60 + ? FRAME_RATE_CATEGORY_HIGH : FRAME_RATE_CATEGORY_NORMAL; + } int frameRateCategory = mIsTouchBoosting - ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory; + ? FRAME_RATE_CATEGORY_HIGH_HINT + : Math.max(preferredFrameRateCategory, categoryFromConflictedFrameRates); // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction. @@ -12338,7 +12354,7 @@ public final class ViewRootImpl implements ViewParent, + preferredFrameRate); } mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate, - Surface.FRAME_RATE_COMPATIBILITY_DEFAULT).applyAsyncUnsafe(); + mFrameRateCompatibility).applyAsyncUnsafe(); mLastPreferredFrameRate = preferredFrameRate; } } catch (Exception e) { @@ -12361,7 +12377,8 @@ public final class ViewRootImpl implements ViewParent, private boolean shouldSetFrameRate() { // use toolkitSetFrameRate flag to gate the change - return mSurface.isValid() && mPreferredFrameRate > 0 && shouldEnableDvrr(); + return mSurface.isValid() && mPreferredFrameRate > 0 + && shouldEnableDvrr() && !mIsFrameRateConflicted; } private boolean shouldTouchBoost(int motionEventAction, int windowType) { @@ -12404,29 +12421,45 @@ public final class ViewRootImpl implements ViewParent, } /** - * Allow Views to vote for the preferred frame rate + * Allow Views to vote for the preferred frame rate and compatibility. * When determining the preferred frame rate value, * we follow this logic: If no preferred frame rate has been set yet, * we assign the value of frameRate as the preferred frame rate. - * If either the current or the new preferred frame rate exceeds 60 Hz, - * we select the higher value between them. - * However, if both values are 60 Hz or lower, we set the preferred frame rate - * to 60 Hz to maintain optimal performance. + * IF there are multiple frame rates are voted: + * 1. There is a frame rate is a multiple of all other frame rates. + * We choose this frmae rate to be the one to be set. + * 2. There is no frame rate can be a multiple of others + * We set category to HIGH if the maximum frame rate is greater than 60. + * Otherwise, we set category to NORMAL. + * + * Use FRAME_RATE_COMPATIBILITY_GTE for velocity and FRAME_RATE_COMPATIBILITY_FIXED_SOURCE + * for TextureView video play and user requested frame rate. * * @param frameRate the preferred frame rate of a View + * @param frameRateCompatibility the preferred frame rate compatibility of a View */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) - public void votePreferredFrameRate(float frameRate) { + public void votePreferredFrameRate(float frameRate, int frameRateCompatibility) { if (frameRate <= 0) { return; } + if (mPreferredFrameRate > 0 && mPreferredFrameRate % frameRate != 0 + && frameRate % mPreferredFrameRate != 0) { + mIsFrameRateConflicted = true; + mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; + } + if (frameRate > mPreferredFrameRate) { + mFrameRateCompatibility = frameRateCompatibility; + } mPreferredFrameRate = Math.max(mPreferredFrameRate, frameRate); - mHasInvalidation = true; - mHandler.removeMessages(MSG_FRAME_RATE_SETTING); - mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING, - FRAME_RATE_SETTING_REEVALUATE_TIME); + + if (!mIsFrameRateConflicted) { + mHandler.removeMessages(MSG_FRAME_RATE_SETTING); + mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING, + FRAME_RATE_SETTING_REEVALUATE_TIME); + } } /** @@ -12454,6 +12487,14 @@ public final class ViewRootImpl implements ViewParent, } /** + * Get the value of mFrameRateCompatibility + */ + @VisibleForTesting + public int getFrameRateCompatibility() { + return mFrameRateCompatibility; + } + + /** * Get the value of mIsFrameRateBoosting */ @VisibleForTesting @@ -12503,6 +12544,15 @@ public final class ViewRootImpl implements ViewParent, } /** + * Get the value of mIsFrameRateConflicted + * Can be used to checked if there is a conflict of frame rate votes. + */ + @VisibleForTesting + public boolean isFrameRateConflicted() { + return mIsFrameRateConflicted; + } + + /** * Set the value of mIsFrameRatePowerSavingsBalanced * Can be used to checked if toolkit dVRR feature is enabled. */ diff --git a/core/java/android/view/inputmethod/InputBinding.java b/core/java/android/view/inputmethod/InputBinding.java index 2bfeb5abb395..fedee9de1372 100644 --- a/core/java/android/view/inputmethod/InputBinding.java +++ b/core/java/android/view/inputmethod/InputBinding.java @@ -19,11 +19,13 @@ package android.view.inputmethod; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; /** * Information given to an {@link InputMethod} about a client connecting * to it. */ +@RavenwoodKeepWholeClass public final class InputBinding implements Parcelable { static final String TAG = "InputBinding"; diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 52384791f74b..3bce155049c8 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -462,8 +462,8 @@ public final class InputMethodManager { * Flag indicating that views from the default home screen ({@link Intent#CATEGORY_HOME}) may * act as a handwriting delegator for the delegate editor view. If set, views from the home * screen package will be trusted for handwriting delegation, in addition to views in the {@code - * delegatorPackageName} passed to {@link #acceptStylusHandwritingDelegation(View, String, - * int)}. + * delegatorPackageName} passed to + * {@link #acceptStylusHandwritingDelegation(View, String, int, Executor, Consumer)} . */ @FlaggedApi(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) public static final int HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED = 0x0001; @@ -2896,6 +2896,8 @@ public final class InputMethodManager { * @param delegateView delegate view capable of receiving input via {@link InputConnection} * @param delegatorPackageName package name of the delegator that handled initial stylus stroke. * @param flags {@link #HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED} or {@code 0} + * @param executor The executor to run the callback on. + * @param callback {@code true>} would be received if delegation was accepted. * @return {@code true} if view belongs to allowed delegate package declared in {@link * #prepareStylusHandwritingDelegation(View, String)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View, String) @@ -2908,13 +2910,16 @@ public final class InputMethodManager { // session to the delegate view. // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, // CursorAnchorInfo, String) + // @FlaggedApi(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) - public boolean acceptStylusHandwritingDelegation( + public void acceptStylusHandwritingDelegation( @NonNull View delegateView, @NonNull String delegatorPackageName, - @HandwritingDelegateFlags int flags) { + @HandwritingDelegateFlags int flags, @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<Boolean> callback) { Objects.requireNonNull(delegatorPackageName); - return startStylusHandwritingInternal(delegateView, delegatorPackageName, flags); + startStylusHandwritingInternal( + delegateView, delegatorPackageName, flags, executor, callback); } /** diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 55986e7ada47..8d3920f8b1da 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -78,3 +78,11 @@ flag { bug: "300979854" is_fixed_read_only: true } + +flag { + name: "predictive_back_ime" + namespace: "input_method" + description: "Predictive back animation for IMEs" + bug: "322836622" + is_fixed_read_only: true +} diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp index f1b93db3e731..07cbaadf0580 100644 --- a/core/jni/android_view_InputEventReceiver.cpp +++ b/core/jni/android_view_InputEventReceiver.cpp @@ -15,6 +15,7 @@ */ #define LOG_TAG "InputEventReceiver" +#define ATRACE_TAG ATRACE_TAG_INPUT //#define LOG_NDEBUG 0 @@ -46,6 +47,16 @@ static const char* toString(bool value) { return value ? "true" : "false"; } +/** + * Trace a bool variable, writing "1" if the value is "true" and "0" otherwise. + * TODO(b/311142655): delete this tracing. It's only useful for debugging very specific issues. + * @param var the name of the variable + * @param value the value of the variable + */ +static void traceBoolVariable(const char* var, bool value) { + ATRACE_INT(var, value ? 1 : 0); +} + static struct { jclass clazz; @@ -130,6 +141,7 @@ NativeInputEventReceiver::NativeInputEventReceiver( mMessageQueue(messageQueue), mBatchedInputEventPending(false), mFdEvents(0) { + traceBoolVariable("mBatchedInputEventPending", mBatchedInputEventPending); if (kDebugDispatchCycle) { ALOGD("channel '%s' ~ Initializing input event receiver.", getInputChannelName().c_str()); } @@ -311,6 +323,7 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, if (consumeBatches) { mBatchedInputEventPending = false; + traceBoolVariable("mBatchedInputEventPending", mBatchedInputEventPending); } if (outConsumedBatch) { *outConsumedBatch = false; @@ -344,6 +357,7 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, } mBatchedInputEventPending = true; + traceBoolVariable("mBatchedInputEventPending", mBatchedInputEventPending); if (kDebugDispatchCycle) { ALOGD("channel '%s' ~ Dispatching batched input event pending notification.", getInputChannelName().c_str()); @@ -355,6 +369,7 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, if (env->ExceptionCheck()) { ALOGE("Exception dispatching batched input events."); mBatchedInputEventPending = false; // try again later + traceBoolVariable("mBatchedInputEventPending", mBatchedInputEventPending); } } return OK; diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 038c00e3823c..64cbe7f1b079 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -24,6 +24,8 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; +import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; +import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; @@ -705,17 +707,51 @@ public class ViewRootImplTest { public void votePreferredFrameRate_voteFrameRate_aggregate() { View view = new View(sContext); attachViewToWindow(view); + ViewRootImpl viewRootImpl = view.getViewRootImpl(); sInstrumentation.runOnMainSync(() -> { - ViewRootImpl viewRootImpl = view.getViewRootImpl(); assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1); - viewRootImpl.votePreferredFrameRate(24); + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); + assertEquals(viewRootImpl.isFrameRateConflicted(), false); + viewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_GTE); assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1); - viewRootImpl.votePreferredFrameRate(30); + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_GTE); + assertEquals(viewRootImpl.isFrameRateConflicted(), false); + viewRootImpl.votePreferredFrameRate(30, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); assertEquals(viewRootImpl.getPreferredFrameRate(), 30, 0.1); - viewRootImpl.votePreferredFrameRate(60); + // If there is a conflict, then set compatibility to + // FRAME_RATE_COMPATIBILITY_FIXED_SOURCE + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); + // Should be true since there is a conflict between 24 and 30. + assertEquals(viewRootImpl.isFrameRateConflicted(), true); + view.invalidate(); + }); + sInstrumentation.waitForIdleSync(); + + sInstrumentation.runOnMainSync(() -> { + assertEquals(viewRootImpl.isFrameRateConflicted(), false); + viewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_GTE); assertEquals(viewRootImpl.getPreferredFrameRate(), 60, 0.1); - viewRootImpl.votePreferredFrameRate(120); + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_GTE); + assertEquals(viewRootImpl.isFrameRateConflicted(), false); + viewRootImpl.votePreferredFrameRate(120, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); assertEquals(viewRootImpl.getPreferredFrameRate(), 120, 0.1); + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); + // Should be false since 60 is a divisor of 120. + assertEquals(viewRootImpl.isFrameRateConflicted(), false); + viewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_GTE); + assertEquals(viewRootImpl.getPreferredFrameRate(), 120, 0.1); + // compatibility should be remained the same (FRAME_RATE_COMPATIBILITY_FIXED_SOURCE) + // since the frame rate 60 is smaller than 120. + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); + // Should be false since 60 is a divisor of 120. + assertEquals(viewRootImpl.isFrameRateConflicted(), false); + }); } @@ -842,14 +878,26 @@ public class ViewRootImplTest { sInstrumentation.runOnMainSync(() -> { assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1); - viewRootImpl.votePreferredFrameRate(24); + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); + assertEquals(viewRootImpl.isFrameRateConflicted(), false); + viewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1); + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); + assertEquals(viewRootImpl.isFrameRateConflicted(), false); view.invalidate(); assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1); + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); + assertEquals(viewRootImpl.isFrameRateConflicted(), false); }); Thread.sleep(delay); assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1); + assertEquals(viewRootImpl.getFrameRateCompatibility(), + FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); + assertEquals(viewRootImpl.isFrameRateConflicted(), false); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 34be9b097e81..2606fb661e80 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -114,6 +114,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont /** Tracks if we should start the back gesture on the next motion move event */ private boolean mShouldStartOnNextMoveEvent = false; private boolean mOnBackStartDispatched = false; + private boolean mPointerPilfered = false; private final FlingAnimationUtils mFlingAnimationUtils; @@ -404,11 +405,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting void onPilferPointers() { + mPointerPilfered = true; // Dispatch onBackStarted, only to app callbacks. // System callbacks will receive onBackStarted when the remote animation starts. if (!shouldDispatchToAnimator() && mActiveCallback != null) { mCurrentTracker.updateStartLocation(); - tryDispatchAppOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null)); + tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null)); } } @@ -511,7 +513,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); // App is handling back animation. Cancel system animation latency tracking. cancelLatencyTracking(); - tryDispatchAppOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null)); + tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null)); } } @@ -555,14 +557,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont && mBackNavigationInfo.isPrepareRemoteAnimation(); } - private void tryDispatchAppOnBackStarted( + private void tryDispatchOnBackStarted( IOnBackInvokedCallback callback, BackMotionEvent backEvent) { - if (mOnBackStartDispatched && callback != null) { + if (mOnBackStartDispatched || callback == null || !mPointerPilfered) { return; } dispatchOnBackStarted(callback, backEvent); - mOnBackStartDispatched = true; } private void dispatchOnBackStarted( @@ -573,6 +574,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } try { callback.onBackStarted(backEvent); + mOnBackStartDispatched = true; } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } @@ -872,6 +874,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mActiveCallback = null; mShouldStartOnNextMoveEvent = false; mOnBackStartDispatched = false; + mPointerPilfered = false; mShellBackAnimationRegistry.resetDefaultCrossActivity(); cancelLatencyTracking(); if (mBackNavigationInfo != null) { @@ -957,15 +960,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mCurrentTracker.updateStartLocation(); BackMotionEvent startEvent = mCurrentTracker.createStartEvent(apps[0]); - // {@code mActiveCallback} is the callback from - // the BackAnimationRunners and not a real app-side - // callback. We also dispatch to the app-side callback - // (which should be a system callback with PRIORITY_SYSTEM) - // to keep consistent with app registered callbacks. dispatchOnBackStarted(mActiveCallback, startEvent); - tryDispatchAppOnBackStarted( - mBackNavigationInfo.getOnBackInvokedCallback(), - startEvent); } // Dispatch the first progress after animation start for diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index e0f0556d03f0..369258e15454 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -683,6 +683,17 @@ public class BubbleController implements ConfigurationChangeListener, mDataRepository.removeBubblesForUser(removedUserId, parentUserId); } + /** Called when sensitive notification state has changed */ + public void onSensitiveNotificationProtectionStateChanged( + boolean sensitiveNotificationProtectionActive) { + if (mStackView != null) { + mStackView.onSensitiveNotificationProtectionStateChanged( + sensitiveNotificationProtectionActive); + ProtoLog.d(WM_SHELL_BUBBLES, "onSensitiveNotificationProtectionStateChanged=%b", + sensitiveNotificationProtectionActive); + } + } + /** Whether bubbles are showing in the bubble bar. */ public boolean isShowingAsBubbleBar() { return canShowAsBubbleBar() && mBubbleStateListener != null; @@ -2583,6 +2594,14 @@ public class BubbleController implements ConfigurationChangeListener, mMainExecutor.execute( () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded)); } + + @Override + public void onSensitiveNotificationProtectionStateChanged( + boolean sensitiveNotificationProtectionActive) { + mMainExecutor.execute( + () -> BubbleController.this.onSensitiveNotificationProtectionStateChanged( + sensitiveNotificationProtectionActive)); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index b23fd5269eae..e7da034a7422 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -291,6 +291,11 @@ public class BubbleStackView extends FrameLayout */ private boolean mRemovingLastBubbleWhileExpanded = false; + /** + * Whether sensitive notification protection should disable flyout + */ + private boolean mSensitiveNotificationProtectionActive = false; + /** Animator for animating the expanded view's alpha (including the TaskView inside it). */ private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); @@ -2199,6 +2204,11 @@ public class BubbleStackView extends FrameLayout } } + void onSensitiveNotificationProtectionStateChanged( + boolean sensitiveNotificationProtectionActive) { + mSensitiveNotificationProtectionActive = sensitiveNotificationProtectionActive; + } + /** * Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or * not. @@ -2842,6 +2852,7 @@ public class BubbleStackView extends FrameLayout || isExpanded() || mIsExpansionAnimating || mIsGestureInProgress + || mSensitiveNotificationProtectionActive || mBubbleToExpandAfterFlyoutCollapse != null || bubbleView == null) { if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 28af0ca6ac6c..26077cf7057b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -286,6 +286,16 @@ public interface Bubbles { void onUserRemoved(int removedUserId); /** + * Called when the Sensitive notification protection state has changed, such as when media + * projection starts and stops. + * + * @param sensitiveNotificationProtectionActive {@code true} if notifications should be + * protected + */ + void onSensitiveNotificationProtectionStateChanged( + boolean sensitiveNotificationProtectionActive); + + /** * A listener to be notified of bubble state changes, used by launcher to render bubbles in * its process. */ diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index e4f3e2defb25..abd84de7da3c 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -155,6 +155,7 @@ cc_defaults { host: { static_libs: [ "libandroidfw", + "libhostgraphics", "libutils", ], }, @@ -501,6 +502,17 @@ cc_library_headers { ], header_libs: ["android_graphics_jni_headers"], export_header_lib_headers: ["android_graphics_jni_headers"], + target: { + android: { + export_include_dirs: ["platform/android"], + }, + host: { + export_include_dirs: ["platform/host"], + }, + windows: { + enabled: true, + }, + }, } cc_defaults { @@ -538,6 +550,7 @@ cc_defaults { "utils/Blur.cpp", "utils/Color.cpp", "utils/LinearAllocator.cpp", + "utils/StringUtils.cpp", "utils/TypefaceUtils.cpp", "utils/VectorDrawableUtils.cpp", "AnimationContext.cpp", @@ -552,6 +565,7 @@ cc_defaults { "Mesh.cpp", "MemoryPolicy.cpp", "PathParser.cpp", + "ProfileData.cpp", "Properties.cpp", "PropertyValuesAnimatorSet.cpp", "PropertyValuesHolder.cpp", @@ -569,12 +583,13 @@ cc_defaults { export_proto_headers: true, }, + header_libs: ["libandroid_headers_private"], + target: { android: { - header_libs: [ - "libandroid_headers_private", - "libtonemap_headers", - ], + header_libs: ["libtonemap_headers"], + + local_include_dirs: ["platform/android"], srcs: [ "hwui/AnimatedImageThread.cpp", @@ -605,7 +620,6 @@ cc_defaults { "thread/CommonPool.cpp", "utils/GLUtils.cpp", "utils/NdkUtils.cpp", - "utils/StringUtils.cpp", "AutoBackendTextureRelease.cpp", "DeferredLayerUpdater.cpp", "DeviceInfo.cpp", @@ -617,7 +631,6 @@ cc_defaults { "FrameMetricsReporter.cpp", "Layer.cpp", "LayerUpdateQueue.cpp", - "ProfileData.cpp", "ProfileDataContainer.cpp", "Readback.cpp", "TreeInfo.cpp", @@ -628,6 +641,21 @@ cc_defaults { // Allow implicit fallthroughs in HardwareBitmapUploader.cpp until they are fixed. cflags: ["-Wno-implicit-fallthrough"], }, + host: { + header_libs: ["libnativebase_headers"], + + local_include_dirs: ["platform/host"], + + srcs: [ + "platform/host/renderthread/CacheManager.cpp", + "platform/host/renderthread/RenderThread.cpp", + "platform/host/ProfileDataContainer.cpp", + "platform/host/Readback.cpp", + "platform/host/WebViewFunctorManager.cpp", + ], + + cflags: ["-Wno-unused-private-field"], + }, }, } @@ -663,6 +691,7 @@ cc_defaults { header_libs: ["libandroid_headers_private"], target: { android: { + local_include_dirs: ["platform/android"], shared_libs: [ "libgui", "libui", diff --git a/libs/hwui/ProfileData.cpp b/libs/hwui/ProfileData.cpp index 3d0ca0a10851..7be9541f6b99 100644 --- a/libs/hwui/ProfileData.cpp +++ b/libs/hwui/ProfileData.cpp @@ -110,6 +110,7 @@ void ProfileData::mergeWith(const ProfileData& other) { } void ProfileData::dump(int fd) const { +#ifdef __ANDROID__ dprintf(fd, "\nStats since: %" PRIu64 "ns", mStatStartTime); dprintf(fd, "\nTotal frames rendered: %u", mTotalFrameCount); dprintf(fd, "\nJanky frames: %u (%.2f%%)", mJankFrameCount, @@ -138,6 +139,7 @@ void ProfileData::dump(int fd) const { dprintf(fd, " %ums=%u", entry.renderTimeMs, entry.frameCount); }); dprintf(fd, "\n"); +#endif } uint32_t ProfileData::findPercentile(int percentile) const { diff --git a/libs/hwui/thread/ThreadBase.h b/libs/hwui/platform/android/thread/ThreadBase.h index 0289d3fd2ef7..2f3581f8b355 100644 --- a/libs/hwui/thread/ThreadBase.h +++ b/libs/hwui/platform/android/thread/ThreadBase.h @@ -17,14 +17,14 @@ #ifndef HWUI_THREADBASE_H #define HWUI_THREADBASE_H -#include "WorkQueue.h" -#include "utils/Macros.h" - #include <utils/Looper.h> #include <utils/Thread.h> #include <algorithm> +#include "thread/WorkQueue.h" +#include "utils/Macros.h" + namespace android::uirenderer { class ThreadBase : public Thread { diff --git a/libs/hwui/platform/host/ProfileDataContainer.cpp b/libs/hwui/platform/host/ProfileDataContainer.cpp new file mode 100644 index 000000000000..9ed1b02a8082 --- /dev/null +++ b/libs/hwui/platform/host/ProfileDataContainer.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProfileDataContainer.h" + +#include <log/log.h> + +namespace android { +namespace uirenderer { + +void ProfileDataContainer::freeData() REQUIRES(mJankDataMutex) { + delete mData; + mIsMapped = false; + mData = nullptr; +} + +void ProfileDataContainer::rotateStorage() { + std::lock_guard lock(mJankDataMutex); + mData->reset(); +} + +void ProfileDataContainer::switchStorageToAshmem(int ashmemfd) { + ALOGE("Ashmem is not supported for non-Android configurations"); +} + +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/Readback.cpp b/libs/hwui/platform/host/Readback.cpp new file mode 100644 index 000000000000..b024ec02efc3 --- /dev/null +++ b/libs/hwui/platform/host/Readback.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Readback.h" + +using namespace android::uirenderer::renderthread; + +namespace android { +namespace uirenderer { + +void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<CopyRequest>& request) { +} + +CopyResult Readback::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { + return CopyResult::UnknownError; +} + +CopyResult Readback::copyLayerInto(DeferredLayerUpdater* deferredLayer, SkBitmap* bitmap) { + return CopyResult::UnknownError; +} + +CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap) { + return CopyResult::UnknownError; +} + +CopyResult Readback::copyImageInto(const sk_sp<SkImage>& image, const Rect& srcRect, + SkBitmap* bitmap) { + return CopyResult::UnknownError; +} + +bool Readback::copyLayerInto(Layer* layer, const SkRect* srcRect, const SkRect* dstRect, + SkBitmap* bitmap) { + return false; +} + +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/WebViewFunctorManager.cpp b/libs/hwui/platform/host/WebViewFunctorManager.cpp new file mode 100644 index 000000000000..1d16655bf73c --- /dev/null +++ b/libs/hwui/platform/host/WebViewFunctorManager.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WebViewFunctorManager.h" + +namespace android::uirenderer { + +WebViewFunctor::WebViewFunctor(void* data, const WebViewFunctorCallbacks& callbacks, + RenderMode functorMode) + : mData(data) {} + +WebViewFunctor::~WebViewFunctor() {} + +void WebViewFunctor::sync(const WebViewSyncData& syncData) const {} + +void WebViewFunctor::onRemovedFromTree() {} + +bool WebViewFunctor::prepareRootSurfaceControl() { + return true; +} + +void WebViewFunctor::drawGl(const DrawGlInfo& drawInfo) {} + +void WebViewFunctor::initVk(const VkFunctorInitParams& params) {} + +void WebViewFunctor::drawVk(const VkFunctorDrawParams& params) {} + +void WebViewFunctor::postDrawVk() {} + +void WebViewFunctor::destroyContext() {} + +void WebViewFunctor::removeOverlays() {} + +ASurfaceControl* WebViewFunctor::getSurfaceControl() { + return mSurfaceControl; +} + +void WebViewFunctor::mergeTransaction(ASurfaceTransaction* transaction) {} + +void WebViewFunctor::reparentSurfaceControl(ASurfaceControl* parent) {} + +WebViewFunctorManager& WebViewFunctorManager::instance() { + static WebViewFunctorManager sInstance; + return sInstance; +} + +int WebViewFunctorManager::createFunctor(void* data, const WebViewFunctorCallbacks& callbacks, + RenderMode functorMode) { + return 0; +} + +void WebViewFunctorManager::releaseFunctor(int functor) {} + +void WebViewFunctorManager::onContextDestroyed() {} + +void WebViewFunctorManager::destroyFunctor(int functor) {} + +sp<WebViewFunctor::Handle> WebViewFunctorManager::handleFor(int functor) { + return nullptr; +} + +} // namespace android::uirenderer diff --git a/libs/hwui/platform/host/renderthread/CacheManager.cpp b/libs/hwui/platform/host/renderthread/CacheManager.cpp new file mode 100644 index 000000000000..b03f400cd7d6 --- /dev/null +++ b/libs/hwui/platform/host/renderthread/CacheManager.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "renderthread/CacheManager.h" + +namespace android { +namespace uirenderer { +namespace renderthread { + +CacheManager::CacheManager(RenderThread& thread) + : mRenderThread(thread), mMemoryPolicy(loadMemoryPolicy()) {} + +void CacheManager::setupCacheLimits() {} + +void CacheManager::destroy() {} + +void CacheManager::trimMemory(TrimLevel mode) {} + +void CacheManager::trimCaches(CacheTrimLevel mode) {} + +void CacheManager::trimStaleResources() {} + +void CacheManager::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) {} + +void CacheManager::dumpMemoryUsage(String8& log, const RenderState* renderState) {} + +void CacheManager::onFrameCompleted() {} + +void CacheManager::onThreadIdle() {} + +void CacheManager::scheduleDestroyContext() {} + +void CacheManager::cancelDestroyContext() {} + +bool CacheManager::areAllContextsStopped() { + return false; +} + +void CacheManager::checkUiHidden() {} + +void CacheManager::registerCanvasContext(CanvasContext* context) {} + +void CacheManager::unregisterCanvasContext(CanvasContext* context) {} + +void CacheManager::onContextStopped(CanvasContext* context) {} + +void CacheManager::notifyNextFrameSize(int width, int height) {} + +} /* namespace renderthread */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/renderthread/RenderThread.cpp b/libs/hwui/platform/host/renderthread/RenderThread.cpp new file mode 100644 index 000000000000..6f08b5979772 --- /dev/null +++ b/libs/hwui/platform/host/renderthread/RenderThread.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "renderthread/RenderThread.h" + +#include "Readback.h" +#include "renderthread/VulkanManager.h" + +namespace android { +namespace uirenderer { +namespace renderthread { + +static bool gHasRenderThreadInstance = false; +static JVMAttachHook gOnStartHook = nullptr; + +ASurfaceControlFunctions::ASurfaceControlFunctions() {} + +bool RenderThread::hasInstance() { + return gHasRenderThreadInstance; +} + +void RenderThread::setOnStartHook(JVMAttachHook onStartHook) { + LOG_ALWAYS_FATAL_IF(hasInstance(), "can't set an onStartHook after we've started..."); + gOnStartHook = onStartHook; +} + +JVMAttachHook RenderThread::getOnStartHook() { + return gOnStartHook; +} + +RenderThread& RenderThread::getInstance() { + [[clang::no_destroy]] static sp<RenderThread> sInstance = []() { + sp<RenderThread> thread = sp<RenderThread>::make(); + thread->start("RenderThread"); + return thread; + }(); + gHasRenderThreadInstance = true; + return *sInstance; +} + +RenderThread::RenderThread() + : ThreadBase() + , mVsyncSource(nullptr) + , mVsyncRequested(false) + , mFrameCallbackTaskPending(false) + , mRenderState(nullptr) + , mEglManager(nullptr) + , mFunctorManager(WebViewFunctorManager::instance()) + , mGlobalProfileData(mJankDataMutex) { + Properties::load(); +} + +RenderThread::~RenderThread() {} + +void RenderThread::initThreadLocals() { + mCacheManager = new CacheManager(*this); +} + +void RenderThread::requireGlContext() {} + +void RenderThread::requireVkContext() {} + +void RenderThread::initGrContextOptions(GrContextOptions& options) {} + +void RenderThread::destroyRenderingContext() {} + +VulkanManager& RenderThread::vulkanManager() { + return *mVkManager; +} + +void RenderThread::dumpGraphicsMemory(int fd, bool includeProfileData) {} + +void RenderThread::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) {} + +Readback& RenderThread::readback() { + if (!mReadback) { + mReadback = new Readback(*this); + } + + return *mReadback; +} + +void RenderThread::setGrContext(sk_sp<GrDirectContext> context) {} + +sk_sp<GrDirectContext> RenderThread::requireGrContext() { + return mGrContext; +} + +bool RenderThread::threadLoop() { + if (gOnStartHook) { + gOnStartHook("RenderThread"); + } + initThreadLocals(); + + while (true) { + waitForWork(); + processQueue(); + mCacheManager->onThreadIdle(); + } + + return false; +} + +void RenderThread::postFrameCallback(IFrameCallback* callback) {} + +bool RenderThread::removeFrameCallback(IFrameCallback* callback) { + return false; +} + +void RenderThread::pushBackFrameCallback(IFrameCallback* callback) {} + +sk_sp<Bitmap> RenderThread::allocateHardwareBitmap(SkBitmap& skBitmap) { + return nullptr; +} + +bool RenderThread::isCurrent() { + return true; +} + +void RenderThread::preload() {} + +void RenderThread::trimMemory(TrimLevel level) {} + +void RenderThread::trimCaches(CacheTrimLevel level) {} + +} /* namespace renderthread */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/thread/ThreadBase.h b/libs/hwui/platform/host/thread/ThreadBase.h new file mode 100644 index 000000000000..d709430cc9b6 --- /dev/null +++ b/libs/hwui/platform/host/thread/ThreadBase.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HWUI_THREADBASE_H +#define HWUI_THREADBASE_H + +#include <utils/Thread.h> + +#include <algorithm> + +#include "thread/WorkQueue.h" +#include "utils/Macros.h" + +namespace android::uirenderer { + +class ThreadBase : public Thread { + PREVENT_COPY_AND_ASSIGN(ThreadBase); + +public: + ThreadBase() : Thread(false), mQueue([this]() { mCondition.notify_all(); }, mLock) {} + + WorkQueue& queue() { return mQueue; } + + void requestExit() { Thread::requestExit(); } + + void start(const char* name = "ThreadBase") { Thread::run(name); } + + void join() { Thread::join(); } + + bool isRunning() const { return Thread::isRunning(); } + +protected: + void waitForWork() { + std::unique_lock lock{mLock}; + nsecs_t nextWakeup = mQueue.nextWakeup(lock); + std::chrono::nanoseconds duration = std::chrono::nanoseconds::max(); + if (nextWakeup < std::numeric_limits<nsecs_t>::max()) { + int timeout = nextWakeup - WorkQueue::clock::now(); + if (timeout < 0) timeout = 0; + duration = std::chrono::nanoseconds(timeout); + } + mCondition.wait_for(lock, duration); + } + + void processQueue() { mQueue.process(); } + + virtual bool threadLoop() override { + while (!exitPending()) { + waitForWork(); + processQueue(); + } + return false; + } + +private: + WorkQueue mQueue; + std::mutex mLock; + std::condition_variable mCondition; +}; + +} // namespace android::uirenderer + +#endif // HWUI_THREADBASE_H diff --git a/libs/hwui/private/hwui/WebViewFunctor.h b/libs/hwui/private/hwui/WebViewFunctor.h index 22ae59e5137b..493c943079ab 100644 --- a/libs/hwui/private/hwui/WebViewFunctor.h +++ b/libs/hwui/private/hwui/WebViewFunctor.h @@ -17,15 +17,7 @@ #ifndef FRAMEWORKS_BASE_WEBVIEWFUNCTOR_H #define FRAMEWORKS_BASE_WEBVIEWFUNCTOR_H -#ifdef __ANDROID__ // Layoutlib does not support surface control #include <android/surface_control.h> -#else -// To avoid ifdefs around overlay implementation all over the place we typedef these to void *. They -// won't be used. -typedef void* ASurfaceControl; -typedef void* ASurfaceTransaction; -#endif - #include <cutils/compiler.h> #include <private/hwui/DrawGlInfo.h> #include <private/hwui/DrawVkInfo.h> diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index 623ee4e6c27e..a024aeb285f9 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -17,11 +17,12 @@ #include "RenderThread.h" #include <GrContextOptions.h> -#include <include/gpu/ganesh/gl/GrGLDirectContext.h> #include <android-base/properties.h> #include <dlfcn.h> #include <gl/GrGLInterface.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/gl/GrGLDirectContext.h> +#include <private/android/choreographer.h> #include <sys/resource.h> #include <ui/FatVector.h> #include <utils/Condition.h> diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h index 79e57de9d66f..045d26f1d329 100644 --- a/libs/hwui/renderthread/RenderThread.h +++ b/libs/hwui/renderthread/RenderThread.h @@ -20,10 +20,7 @@ #include <GrDirectContext.h> #include <SkBitmap.h> #include <cutils/compiler.h> -#include <private/android/choreographer.h> #include <surface_control_private.h> -#include <thread/ThreadBase.h> -#include <utils/Looper.h> #include <utils/Thread.h> #include <memory> diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java index 5242a7d32930..c81b95b7c81b 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -176,11 +176,25 @@ public final class ApduServiceInfo implements Parcelable { List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups, boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid, String settingsActivityName, String offHost, String staticOffHost, boolean isEnabled) { + this(info, onHost, description, staticAidGroups, dynamicAidGroups, + requiresUnlock, requiresScreenOn, bannerResource, uid, + settingsActivityName, offHost, staticOffHost, isEnabled, + new HashMap<String, Boolean>()); + } + + /** + * @hide + */ + public ApduServiceInfo(ResolveInfo info, boolean onHost, String description, + List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups, + boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid, + String settingsActivityName, String offHost, String staticOffHost, boolean isEnabled, + HashMap<String, Boolean> autoTransact) { this.mService = info; this.mDescription = description; this.mStaticAidGroups = new HashMap<String, AidGroup>(); this.mDynamicAidGroups = new HashMap<String, AidGroup>(); - this.mAutoTransact = new HashMap<String, Boolean>(); + this.mAutoTransact = autoTransact; this.mOffHostName = offHost; this.mStaticOffHostName = staticOffHost; this.mOnHost = onHost; @@ -196,7 +210,6 @@ public final class ApduServiceInfo implements Parcelable { this.mUid = uid; this.mSettingsActivityName = settingsActivityName; this.mCategoryOtherServiceEnabled = isEnabled; - } /** @@ -857,6 +870,8 @@ public final class ApduServiceInfo implements Parcelable { dest.writeString(mSettingsActivityName); dest.writeInt(mCategoryOtherServiceEnabled ? 1 : 0); + dest.writeInt(mAutoTransact.size()); + dest.writeMap(mAutoTransact); }; @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @@ -885,10 +900,15 @@ public final class ApduServiceInfo implements Parcelable { int uid = source.readInt(); String settingsActivityName = source.readString(); boolean isEnabled = source.readInt() != 0; + int autoTransactSize = source.readInt(); + HashMap<String, Boolean> autoTransact = + new HashMap<String, Boolean>(autoTransactSize); + source.readMap(autoTransact, getClass().getClassLoader(), + String.class, Boolean.class); return new ApduServiceInfo(info, onHost, description, staticAidGroups, dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid, settingsActivityName, offHostName, staticOffHostName, - isEnabled); + isEnabled, autoTransact); } @Override diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 25608fedc976..eef75c7b707b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -40,6 +40,7 @@ import android.service.autofill.Field import android.service.autofill.FillCallback import android.service.autofill.FillRequest import android.service.autofill.FillResponse +import android.service.autofill.Flags import android.service.autofill.InlinePresentation import android.service.autofill.Presentations import android.service.autofill.SaveCallback @@ -479,18 +480,28 @@ class CredentialAutofillService : AutofillService() { val autofillIdToCredentialEntries: MutableMap<AutofillId, ArrayList<Entry>> = mutableMapOf() credentialEntryList.forEach entryLoop@{ credentialEntry -> - val autofillId: AutofillId? = credentialEntry - .frameworkExtrasIntent - ?.getParcelableExtra( - CredentialProviderService.EXTRA_AUTOFILL_ID, - AutofillId::class.java) - if (autofillId == null) { - Log.e(TAG, "AutofillId is missing from credential entry. Credential" + - " Integration might be disabled.") - return@entryLoop - } - autofillIdToCredentialEntries.getOrPut(autofillId) { ArrayList() } - .add(credentialEntry) + val intent = credentialEntry.frameworkExtrasIntent + intent?.getParcelableExtra( + CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST, + android.service.credentials.GetCredentialRequest::class.java) + ?.credentialOptions + ?.forEach { credentialOption -> + credentialOption.candidateQueryData.getParcelableArrayList( + CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java) + ?.forEach { autofillId -> + intent.putExtra( + CredentialProviderService.EXTRA_AUTOFILL_ID, + autofillId) + val entry = Entry( + credentialEntry.key, + credentialEntry.subkey, + credentialEntry.slice, + intent) + autofillIdToCredentialEntries + .getOrPut(autofillId) { ArrayList() } + .add(entry) + } + } } return autofillIdToCredentialEntries } @@ -573,23 +584,31 @@ class CredentialAutofillService : AutofillService() { cmRequests: MutableList<CredentialOption>, responseClientState: Bundle ) { + val traversedViewNodes: MutableSet<AutofillId> = mutableSetOf() + val credentialOptionsFromHints: MutableMap<String, CredentialOption> = mutableMapOf() val windowNodes: List<AssistStructure.WindowNode> = structure.run { (0 until windowNodeCount).map { getWindowNodeAt(it) } } windowNodes.forEach { windowNode: AssistStructure.WindowNode -> - traverseNodeForRequest(windowNode.rootViewNode, cmRequests, responseClientState) + traverseNodeForRequest( + windowNode.rootViewNode, cmRequests, responseClientState, traversedViewNodes, + credentialOptionsFromHints) } } private fun traverseNodeForRequest( viewNode: AssistStructure.ViewNode, cmRequests: MutableList<CredentialOption>, - responseClientState: Bundle + responseClientState: Bundle, + traversedViewNodes: MutableSet<AutofillId>, + credentialOptionsFromHints: MutableMap<String, CredentialOption> ) { viewNode.autofillId?.let { - cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState)) + cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState, + traversedViewNodes, credentialOptionsFromHints)) + traversedViewNodes.add(it) } val children: List<AssistStructure.ViewNode> = @@ -598,26 +617,37 @@ class CredentialAutofillService : AutofillService() { } children.forEach { childNode: AssistStructure.ViewNode -> - traverseNodeForRequest(childNode, cmRequests, responseClientState) + traverseNodeForRequest(childNode, cmRequests, responseClientState, traversedViewNodes, + credentialOptionsFromHints) } } private fun getCredentialOptionsFromViewNode( viewNode: AssistStructure.ViewNode, autofillId: AutofillId, - responseClientState: Bundle + responseClientState: Bundle, + traversedViewNodes: MutableSet<AutofillId>, + credentialOptionsFromHints: MutableMap<String, CredentialOption> ): MutableList<CredentialOption> { - if (viewNode.credentialManagerRequest != null) { - val options = viewNode.credentialManagerRequest?.getCredentialOptions() - if (options != null) { - for (option in options) { - option.candidateQueryData.putParcelable( - CredentialProviderService.EXTRA_AUTOFILL_ID, autofillId - ) - } - return options + val credentialOptions: MutableList<CredentialOption> = mutableListOf() + if (Flags.autofillCredmanDevIntegration() && viewNode.credentialManagerRequest != null) { + viewNode.credentialManagerRequest + ?.getCredentialOptions() + ?.forEach { credentialOption -> + credentialOption.candidateQueryData + .getParcelableArrayList( + CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java) + ?.let { associatedAutofillIds -> + // Check whether any of the associated autofill ids have already been + // traversed. If so, skip, to dedupe on duplicate credential options. + if ((traversedViewNodes intersect associatedAutofillIds.toSet()) + .isEmpty()) { + credentialOptions.add(credentialOption) + } + } } } + // TODO(b/325502552): clean up cred option logic in autofill hint val credentialHints: MutableList<String> = mutableListOf() if (viewNode.autofillHints != null) { @@ -631,10 +661,10 @@ class CredentialAutofillService : AutofillService() { } } - val credentialOptions: MutableList<CredentialOption> = mutableListOf() for (credentialHint in credentialHints) { try { - convertJsonToCredentialOption(credentialHint, autofillId) + convertJsonToCredentialOption( + credentialHint, autofillId, credentialOptionsFromHints) .let { credentialOptions.addAll(it) } } catch (e: JSONException) { Log.i(TAG, "Exception while parsing response: " + e.message) @@ -643,10 +673,11 @@ class CredentialAutofillService : AutofillService() { return credentialOptions } - private fun convertJsonToCredentialOption(jsonString: String, autofillId: AutofillId): - List<CredentialOption> { - // TODO(b/302000646) Move this logic to jetpack so that is consistent - // with building the json + private fun convertJsonToCredentialOption( + jsonString: String, + autofillId: AutofillId, + credentialOptionsFromHints: MutableMap<String, CredentialOption> + ): List<CredentialOption> { val credentialOptions: MutableList<CredentialOption> = mutableListOf() val json = JSONObject(jsonString) @@ -654,16 +685,34 @@ class CredentialAutofillService : AutofillService() { val options = jsonGet.getJSONArray(CRED_OPTIONS_KEY) for (i in 0 until options.length()) { val option = options.getJSONObject(i) - val candidateBundle = convertJsonToBundle(option.getJSONObject(CANDIDATE_DATA_KEY)) - candidateBundle.putParcelable( + val optionString = option.toString() + credentialOptionsFromHints[optionString] + ?.let { credentialOption -> + // if the current credential option was seen before, add the current + // viewNode to the credential option, but do not add it to the option list + // again. This will result in the same result as deduping based on + // traversed viewNode. + credentialOption.candidateQueryData.getParcelableArrayList( + CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java) + ?.let { + it.add(autofillId) + credentialOption.candidateQueryData.putParcelableArrayList( + CredentialProviderService.EXTRA_AUTOFILL_ID, it) + } + } ?: run { + val candidateBundle = convertJsonToBundle(option.getJSONObject(CANDIDATE_DATA_KEY)) + candidateBundle.putParcelableArrayList( CredentialProviderService.EXTRA_AUTOFILL_ID, - autofillId) - credentialOptions.add(CredentialOption( + arrayListOf(autofillId)) + val credentialOption = CredentialOption( option.getString(TYPE_KEY), convertJsonToBundle(option.getJSONObject(REQUEST_DATA_KEY)), candidateBundle, option.getBoolean(SYS_PROVIDER_REQ_KEY), - )) + ) + credentialOptions.add(credentialOption) + credentialOptionsFromHints[optionString] = credentialOption + } } return credentialOptions } diff --git a/packages/PrintRecommendationService/res/values/strings.xml b/packages/PrintRecommendationService/res/values/strings.xml index 2bab1b65529b..b6c45b7a23c8 100644 --- a/packages/PrintRecommendationService/res/values/strings.xml +++ b/packages/PrintRecommendationService/res/values/strings.xml @@ -18,7 +18,6 @@ --> <resources> - <string name="plugin_vendor_google_cloud_print">Cloud Print</string> <string name="plugin_vendor_hp">HP</string> <string name="plugin_vendor_lexmark">Lexmark</string> <string name="plugin_vendor_brother">Brother</string> diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java index 5a756fe50209..4ec88830386b 100644 --- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java +++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java @@ -23,7 +23,6 @@ import android.printservice.recommendation.RecommendationInfo; import android.printservice.recommendation.RecommendationService; import android.util.Log; -import com.android.printservice.recommendation.plugin.google.CloudPrintPlugin; import com.android.printservice.recommendation.plugin.hp.HPRecommendationPlugin; import com.android.printservice.recommendation.plugin.mdnsFilter.MDNSFilterPlugin; import com.android.printservice.recommendation.plugin.mdnsFilter.VendorConfig; @@ -77,14 +76,6 @@ public class RecommendationServiceImpl extends RecommendationService } try { - mPlugins.add(new RemotePrintServicePlugin(new CloudPrintPlugin(this), this, - true)); - } catch (Exception e) { - Log.e(LOG_TAG, "Could not initiate " - + getString(R.string.plugin_vendor_google_cloud_print) + " plugin", e); - } - - try { mPlugins.add(new RemotePrintServicePlugin(new HPRecommendationPlugin(this), this, false)); } catch (Exception e) { diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java deleted file mode 100644 index 3029d10d4cf3..000000000000 --- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.printservice.recommendation.plugin.google; - -import static com.android.printservice.recommendation.util.MDNSUtils.ATTRIBUTE_TY; - -import android.content.Context; -import android.util.ArrayMap; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; - -import com.android.printservice.recommendation.PrintServicePlugin; -import com.android.printservice.recommendation.R; -import com.android.printservice.recommendation.util.MDNSFilteredDiscovery; - -import java.net.Inet4Address; -import java.net.InetAddress; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Plugin detecting <a href="https://developers.google.com/cloud-print/docs/privet">Google Cloud - * Print</a> printers. - */ -public class CloudPrintPlugin implements PrintServicePlugin { - private static final String LOG_TAG = CloudPrintPlugin.class.getSimpleName(); - private static final boolean DEBUG = false; - - private static final String ATTRIBUTE_TXTVERS = "txtvers"; - private static final String ATTRIBUTE_URL = "url"; - private static final String ATTRIBUTE_TYPE = "type"; - private static final String ATTRIBUTE_ID = "id"; - private static final String ATTRIBUTE_CS = "cs"; - - private static final String TYPE = "printer"; - - private static final String PRIVET_SERVICE = "_privet._tcp"; - - /** The required mDNS service types */ - private static final Set<String> PRINTER_SERVICE_TYPE = Set.of( - PRIVET_SERVICE); // Not checking _printer_._sub - - /** All possible connection states */ - private static final Set<String> POSSIBLE_CONNECTION_STATES = Set.of( - "online", - "offline", - "connecting", - "not-configured"); - - private static final byte SUPPORTED_TXTVERS = '1'; - - /** The mDNS filtered discovery */ - private final MDNSFilteredDiscovery mMDNSFilteredDiscovery; - - /** - * Create a plugin detecting Google Cloud Print printers. - * - * @param context The context the plugin runs in - */ - public CloudPrintPlugin(@NonNull Context context) { - mMDNSFilteredDiscovery = new MDNSFilteredDiscovery(context, PRINTER_SERVICE_TYPE, - nsdServiceInfo -> { - // The attributes are case insensitive. For faster searching create a clone of - // the map with the attribute-keys all in lower case. - ArrayMap<String, byte[]> caseInsensitiveAttributes = - new ArrayMap<>(nsdServiceInfo.getAttributes().size()); - for (Map.Entry<String, byte[]> entry : nsdServiceInfo.getAttributes() - .entrySet()) { - caseInsensitiveAttributes.put(entry.getKey().toLowerCase(), - entry.getValue()); - } - - if (DEBUG) { - Log.i(LOG_TAG, nsdServiceInfo.getServiceName() + ":"); - Log.i(LOG_TAG, "type: " + nsdServiceInfo.getServiceType()); - Log.i(LOG_TAG, "host: " + nsdServiceInfo.getHost()); - for (Map.Entry<String, byte[]> entry : caseInsensitiveAttributes.entrySet()) { - if (entry.getValue() == null) { - Log.i(LOG_TAG, entry.getKey() + "= null"); - } else { - Log.i(LOG_TAG, entry.getKey() + "=" + new String(entry.getValue(), - StandardCharsets.UTF_8)); - } - } - } - - byte[] txtvers = caseInsensitiveAttributes.get(ATTRIBUTE_TXTVERS); - if (txtvers == null || txtvers.length != 1 || txtvers[0] != SUPPORTED_TXTVERS) { - // The spec requires this to be the first attribute, but at this time we - // lost the order of the attributes - return false; - } - - if (caseInsensitiveAttributes.get(ATTRIBUTE_TY) == null) { - return false; - } - - byte[] url = caseInsensitiveAttributes.get(ATTRIBUTE_URL); - if (url == null || url.length == 0) { - return false; - } - - byte[] type = caseInsensitiveAttributes.get(ATTRIBUTE_TYPE); - if (type == null || !TYPE.equals( - new String(type, StandardCharsets.UTF_8).toLowerCase())) { - return false; - } - - if (caseInsensitiveAttributes.get(ATTRIBUTE_ID) == null) { - return false; - } - - byte[] cs = caseInsensitiveAttributes.get(ATTRIBUTE_CS); - if (cs == null || !POSSIBLE_CONNECTION_STATES.contains( - new String(cs, StandardCharsets.UTF_8).toLowerCase())) { - return false; - } - - InetAddress address = nsdServiceInfo.getHost(); - if (!(address instanceof Inet4Address)) { - // Not checking for link local address - return false; - } - - return true; - }); - } - - @Override - @NonNull public CharSequence getPackageName() { - return "com.google.android.apps.cloudprint"; - } - - @Override - public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception { - mMDNSFilteredDiscovery.start(callback); - } - - @Override - @StringRes public int getName() { - return R.string.plugin_vendor_google_cloud_print; - } - - @Override - public void stop() throws Exception { - mMDNSFilteredDiscovery.stop(); - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index e1853675d6d4..761bb7918afd 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -27,7 +27,7 @@ import com.android.settingslib.spa.gallery.chart.ChartPageProvider import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.dialog.NavDialogProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider -import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider +import com.android.settingslib.spa.gallery.editor.SettingsDropdownBoxPageProvider import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider @@ -99,7 +99,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { OperateListPageProvider, EditorMainPageProvider, SettingsOutlinedTextFieldPageProvider, - SettingsExposedDropdownMenuBoxPageProvider, + SettingsDropdownBoxPageProvider, SettingsDropdownCheckBoxProvider, SettingsTextFieldPasswordPageProvider, SearchScaffoldPageProvider, diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt index 9f2158a13f25..c511542f265a 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt @@ -35,7 +35,7 @@ object EditorMainPageProvider : SettingsPageProvider { return listOf( SettingsOutlinedTextFieldPageProvider.buildInjectEntry().setLink(fromPage = owner) .build(), - SettingsExposedDropdownMenuBoxPageProvider.buildInjectEntry().setLink(fromPage = owner) + SettingsDropdownBoxPageProvider.buildInjectEntry().setLink(fromPage = owner) .build(), SettingsDropdownCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner) .build(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownBoxPageProvider.kt index 5ffbe8ba8a26..2ebb5f5eba27 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownBoxPageProvider.kt @@ -28,16 +28,15 @@ import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuBox +import com.android.settingslib.spa.widget.editor.SettingsDropdownBox import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.scaffold.RegularScaffold -private const val TITLE = "Sample SettingsExposedDropdownMenuBox" +private const val TITLE = "Sample SettingsDropdownBox" -object SettingsExposedDropdownMenuBoxPageProvider : SettingsPageProvider { - override val name = "SettingsExposedDropdownMenuBox" - private const val exposedDropdownMenuBoxLabel = "ExposedDropdownMenuBoxLabel" +object SettingsDropdownBoxPageProvider : SettingsPageProvider { + override val name = "SettingsDropdownBox" override fun getTitle(arguments: Bundle?): String { return TITLE @@ -45,18 +44,44 @@ object SettingsExposedDropdownMenuBoxPageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { - var selectedItem by remember { mutableIntStateOf(-1) } - val options = listOf("item1", "item2", "item3") RegularScaffold(title = TITLE) { - SettingsExposedDropdownMenuBox( - label = exposedDropdownMenuBoxLabel, - options = options, - selectedOptionIndex = selectedItem, - enabled = true, - onselectedOptionTextChange = { selectedItem = it }) + Regular() + NotEnabled() + Empty() } } + @Composable + private fun Regular() { + var selectedItem by remember { mutableIntStateOf(-1) } + SettingsDropdownBox( + label = "SettingsDropdownBox", + options = listOf("item1", "item2", "item3"), + selectedOptionIndex = selectedItem, + ) { selectedItem = it } + } + + @Composable + private fun NotEnabled() { + var selectedItem by remember { mutableIntStateOf(0) } + SettingsDropdownBox( + label = "Not enabled", + options = listOf("item1", "item2", "item3"), + enabled = false, + selectedOptionIndex = selectedItem, + ) { selectedItem = it } + } + + @Composable + private fun Empty() { + var selectedItem by remember { mutableIntStateOf(-1) } + SettingsDropdownBox( + label = "Empty", + options = emptyList(), + selectedOptionIndex = selectedItem, + ) { selectedItem = it } + } + fun buildInjectEntry(): SettingsEntryBuilder { return SettingsEntryBuilder.createInject(owner = createSettingsPage()) .setUiLayoutFn { @@ -70,8 +95,8 @@ object SettingsExposedDropdownMenuBoxPageProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable -private fun SettingsExposedDropdownMenuBoxPagePreview() { +private fun SettingsDropdownBoxPagePreview() { SettingsTheme { - SettingsExposedDropdownMenuBoxPageProvider.Page(null) + SettingsDropdownBoxPageProvider.Page(null) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt index f6692a356899..679c562ac92d 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ package com.android.settingslib.spa.widget.editor import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults @@ -31,80 +30,58 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.framework.theme.SettingsTheme -@Composable +internal interface DropdownTextBoxScope { + fun dismiss() +} + @OptIn(ExperimentalMaterial3Api::class) -fun SettingsExposedDropdownMenuBox( +@Composable +internal fun DropdownTextBox( label: String, - options: List<String>, - selectedOptionIndex: Int, - enabled: Boolean, - onselectedOptionTextChange: (Int) -> Unit, + text: String, + enabled: Boolean = true, + errorMessage: String? = null, + content: @Composable DropdownTextBoxScope.() -> Unit, ) { var expanded by remember { mutableStateOf(false) } + val scope = remember { + object : DropdownTextBoxScope { + override fun dismiss() { + expanded = false + } + } + } ExposedDropdownMenuBox( expanded = expanded, - onExpandedChange = { expanded = it }, + onExpandedChange = { expanded = enabled && it }, modifier = Modifier - .width(350.dp) - .padding(SettingsDimension.menuFieldPadding), + .padding(SettingsDimension.menuFieldPadding) + .width(Width), ) { OutlinedTextField( // The `menuAnchor` modifier must be passed to the text field for correctness. modifier = Modifier .menuAnchor() .fillMaxWidth(), - value = options.getOrElse(selectedOptionIndex) { "" }, + value = text, onValueChange = { }, label = { Text(text = label) }, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon( - expanded = expanded - ) - }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, singleLine = true, readOnly = true, - enabled = enabled + enabled = enabled, + isError = errorMessage != null, + supportingText = errorMessage?.let { { Text(text = it) } }, ) - if (options.isNotEmpty()) { - ExposedDropdownMenu( - expanded = expanded, - modifier = Modifier - .fillMaxWidth(), - onDismissRequest = { expanded = false }, - ) { - options.forEach { option -> - DropdownMenuItem( - text = { Text(option) }, - onClick = { - onselectedOptionTextChange(options.indexOf(option)) - expanded = false - }, - contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, - ) - } - } - } + ExposedDropdownMenu( + expanded = expanded, + modifier = Modifier.width(Width), + onDismissRequest = { expanded = false }, + ) { scope.content() } } } -@Preview -@Composable -private fun SettingsExposedDropdownMenuBoxsPreview() { - val item1 = "item1" - val item2 = "item2" - val item3 = "item3" - val options = listOf(item1, item2, item3) - SettingsTheme { - SettingsExposedDropdownMenuBox( - label = "ExposedDropdownMenuBoxLabel", - options = options, - selectedOptionIndex = 0, - enabled = true, - onselectedOptionTextChange = {}) - } -}
\ No newline at end of file +private val Width = 310.dp diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt new file mode 100644 index 000000000000..ff141c2b383c --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.editor + +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.theme.SettingsTheme + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun SettingsDropdownBox( + label: String, + options: List<String>, + selectedOptionIndex: Int, + enabled: Boolean = true, + onSelectedOptionChange: (Int) -> Unit, +) { + DropdownTextBox( + label = label, + text = options.getOrElse(selectedOptionIndex) { "" }, + enabled = enabled && options.isNotEmpty(), + ) { + options.forEachIndexed { index, option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { + dismiss() + onSelectedOptionChange(index) + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) + } + } +} + +@Preview +@Composable +private fun SettingsDropdownBoxPreview() { + val item1 = "item1" + val item2 = "item2" + val item3 = "item3" + val options = listOf(item1, item2, item3) + SettingsTheme { + SettingsDropdownBox( + label = "ExposedDropdownMenuBoxLabel", + options = options, + selectedOptionIndex = 0, + enabled = true, + ) {} + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt index 57963e6eaa40..0e7e49960be1 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt @@ -19,28 +19,15 @@ package com.android.settingslib.spa.widget.editor import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material3.Checkbox -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled import com.android.settingslib.spa.framework.theme.SettingsTheme @@ -68,7 +55,6 @@ data class SettingsDropdownCheckOption( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsDropdownCheckBox( label: String, @@ -78,43 +64,18 @@ fun SettingsDropdownCheckBox( errorMessage: String? = null, onSelectedStateChange: () -> Unit = {}, ) { - var dropDownWidth by remember { mutableIntStateOf(0) } - var expanded by remember { mutableStateOf(false) } - val changeable = enabled && options.changeable - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = changeable && it }, - modifier = Modifier - .width(350.dp) - .padding(SettingsDimension.textFieldPadding) - .onSizeChanged { dropDownWidth = it.width }, + DropdownTextBox( + label = label, + text = getDisplayText(options) ?: emptyText, + enabled = enabled && options.changeable, + errorMessage = errorMessage, ) { - OutlinedTextField( - // The `menuAnchor` modifier must be passed to the text field for correctness. - modifier = Modifier - .menuAnchor() - .fillMaxWidth(), - value = getDisplayText(options) ?: emptyText, - onValueChange = {}, - label = { Text(text = label) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, - readOnly = true, - enabled = changeable, - isError = errorMessage != null, - supportingText = errorMessage?.let { { Text(text = it) } }, - ) - ExposedDropdownMenu( - expanded = expanded, - modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }), - onDismissRequest = { expanded = false }, - ) { - for (option in options) { - CheckboxItem(option) { - option.onClick() - if (option.changeable) { - checkboxItemOnClick(options, option) - onSelectedStateChange() - } + for (option in options) { + CheckboxItem(option) { + option.onClick() + if (option.changeable) { + checkboxItemOnClick(options, option) + onSelectedStateChange() } } } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt new file mode 100644 index 000000000000..c34742461774 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.editor + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SettingsDropdownBoxTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun dropdownMenuBox_displayed() { + composeTestRule.setContent { + var selectedItem by remember { mutableStateOf(0) } + SettingsDropdownBox( + label = LABEL, + options = options, + selectedOptionIndex = selectedItem, + ) { selectedItem = it } + } + + composeTestRule.onNodeWithText(LABEL).assertIsDisplayed() + } + + @Test + fun dropdownMenuBox_enabled_expanded() { + composeTestRule.setContent { + var selectedItem by remember { mutableIntStateOf(0) } + SettingsDropdownBox( + label = LABEL, + options = options, + selectedOptionIndex = selectedItem + ) { selectedItem = it } + } + composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist() + + composeTestRule.onNodeWithText(LABEL).performClick() + + composeTestRule.onNodeWithText(ITEM2).assertIsDisplayed() + } + + @Test + fun dropdownMenuBox_notEnabled_notExpanded() { + composeTestRule.setContent { + var selectedItem by remember { mutableIntStateOf(0) } + SettingsDropdownBox( + label = LABEL, + options = options, + enabled = false, + selectedOptionIndex = selectedItem + ) { selectedItem = it } + } + composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist() + + composeTestRule.onNodeWithText(LABEL).performClick() + + composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist() + } + + @Test + fun dropdownMenuBox_valueChanged() { + composeTestRule.setContent { + var selectedItem by remember { mutableIntStateOf(0) } + SettingsDropdownBox( + label = LABEL, + options = options, + selectedOptionIndex = selectedItem + ) { selectedItem = it } + } + composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist() + + composeTestRule.onNodeWithText(LABEL).performClick() + composeTestRule.onNodeWithText(ITEM2).performClick() + + composeTestRule.onNodeWithText(ITEM2).assertIsDisplayed() + } + private companion object { + const val LABEL = "Label" + const val ITEM2 = "item2" + val options = listOf("item1", ITEM2, "item3") + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt deleted file mode 100644 index bc67e4c61ea5..000000000000 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt +++ /dev/null @@ -1,95 +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.settingslib.spa.widget.editor - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class SettingsExposedDropdownMenuBoxTest { - @get:Rule - val composeTestRule = createComposeRule() - private val options = listOf("item1", "item2", "item3") - private val item2 = "item2" - private val exposedDropdownMenuBoxLabel = "ExposedDropdownMenuBoxLabel" - - @Test - fun exposedDropdownMenuBoxs_displayed() { - composeTestRule.setContent { - var selectedItem by remember { mutableStateOf(0) } - SettingsExposedDropdownMenuBox( - label = exposedDropdownMenuBoxLabel, - options = options, - selectedOptionIndex = selectedItem, - enabled = true, - onselectedOptionTextChange = { selectedItem = it }) - } - composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true) - .assertIsDisplayed() - } - - @Test - fun exposedDropdownMenuBoxs_expanded() { - composeTestRule.setContent { - var selectedItem by remember { mutableIntStateOf(0) } - SettingsExposedDropdownMenuBox( - label = exposedDropdownMenuBoxLabel, - options = options, - selectedOptionIndex = selectedItem, - enabled = true, - onselectedOptionTextChange = { selectedItem = it }) - } - composeTestRule.onNodeWithText(item2, substring = true) - .assertDoesNotExist() - composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true) - .performClick() - composeTestRule.onNodeWithText(item2, substring = true) - .assertIsDisplayed() - } - - @Test - fun exposedDropdownMenuBoxs_valueChanged() { - composeTestRule.setContent { - var selectedItem by remember { mutableIntStateOf(0) } - SettingsExposedDropdownMenuBox( - label = exposedDropdownMenuBoxLabel, - options = options, - selectedOptionIndex = selectedItem, - enabled = true, - onselectedOptionTextChange = { selectedItem = it }) - } - composeTestRule.onNodeWithText(item2, substring = true) - .assertDoesNotExist() - composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true) - .performClick() - composeTestRule.onNodeWithText(item2, substring = true) - .performClick() - composeTestRule.onNodeWithText(item2, substring = true) - .assertIsDisplayed() - } -}
\ No newline at end of file diff --git a/packages/SettingsLib/res/drawable/ic_bt_le_audio_sharing.xml b/packages/SettingsLib/res/drawable/ic_bt_le_audio_sharing.xml new file mode 100644 index 000000000000..618677389ce1 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_bt_le_audio_sharing.xml @@ -0,0 +1,87 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true" + android:height="24dp" + android:width="24dp" + android:viewportHeight="24" + android:viewportWidth="24" + android:tint="?android:attr/colorControlNormal"> + <path + android:fillColor="#000000" + android:pathData="M16.984,24H7.279L12.131,15.508L16.984,24ZM10.481,22.144H13.781L12.131,19.257L10.481,22.144Z"/> + <path + android:fillColor="#000000" + android:pathData="M12.131,14.295C13.471,14.295 14.558,13.209 14.558,11.869C14.558,10.529 13.471,9.442 12.131,9.442C10.791,9.442 9.705,10.529 9.705,11.869C9.705,13.209 10.791,14.295 12.131,14.295Z"/> + <path + android:fillColor="#000000" + android:pathData="M4.573,21.368C4.052,20.943 3.967,20.179 4.379,19.657C4.804,19.136 5.568,19.051 6.09,19.463C6.611,19.876 6.696,20.64 6.284,21.174C6.041,21.465 5.689,21.623 5.338,21.623C5.071,21.623 4.804,21.538 4.573,21.368Z"/> + <path + android:fillColor="#000000" + android:pathData="M17.991,21.162C17.579,20.628 17.663,19.876 18.185,19.451C18.707,19.039 19.471,19.124 19.896,19.646C20.308,20.167 20.223,20.931 19.702,21.344C19.471,21.526 19.204,21.611 18.949,21.611C18.586,21.611 18.234,21.453 17.991,21.162Z"/> + <path + android:fillColor="#000000" + android:pathData="M1.213,17.145C0.91,16.551 1.165,15.823 1.771,15.532C2.378,15.241 3.093,15.495 3.397,16.09C3.688,16.697 3.433,17.424 2.827,17.715C2.657,17.8 2.475,17.837 2.305,17.837C1.844,17.837 1.419,17.582 1.213,17.145Z"/> + <path + android:fillColor="#000000" + android:pathData="M21.449,17.691C20.842,17.4 20.588,16.684 20.879,16.077C21.17,15.471 21.898,15.216 22.504,15.507C23.099,15.798 23.354,16.526 23.062,17.133C22.856,17.557 22.419,17.812 21.971,17.812C21.789,17.812 21.619,17.776 21.449,17.691Z"/> + <path + android:fillColor="#000000" + android:pathData="M0,11.892C0,11.225 0.546,10.679 1.213,10.679C1.88,10.679 2.426,11.212 2.426,11.892C2.426,12.559 1.88,13.105 1.213,13.105C0.546,13.105 0,12.559 0,11.892Z"/> + <path + android:fillColor="#000000" + android:pathData="M21.837,11.869C21.837,11.857 21.837,11.845 21.837,11.833C21.824,11.153 22.37,10.62 23.05,10.607C23.717,10.607 24.251,11.153 24.263,11.821C24.263,11.833 24.263,11.845 24.263,11.845C24.263,11.857 24.263,11.869 24.263,11.869C24.263,12.536 23.717,13.082 23.05,13.082C22.382,13.082 21.837,12.536 21.837,11.869Z"/> + <path + android:fillColor="#000000" + android:pathData="M1.759,8.242C1.152,7.963 0.898,7.235 1.189,6.628C1.48,6.022 2.196,5.767 2.802,6.058C3.409,6.349 3.664,7.077 3.372,7.684C3.166,8.108 2.729,8.363 2.281,8.363C2.099,8.363 1.929,8.327 1.759,8.242Z"/> + <path + android:fillColor="#000000" + android:pathData="M20.866,7.622C20.563,7.028 20.818,6.3 21.424,6.009C22.019,5.706 22.747,5.96 23.038,6.567C23.038,6.567 23.038,6.567 23.05,6.567C23.341,7.161 23.087,7.889 22.48,8.181C22.31,8.265 22.128,8.302 21.958,8.302C21.509,8.302 21.073,8.059 20.866,7.622Z"/> + <path + android:fillColor="#000000" + android:pathData="M4.355,4.104C3.931,3.582 4.016,2.818 4.537,2.406C5.071,1.981 5.823,2.066 6.248,2.588C6.672,3.109 6.588,3.874 6.066,4.298C5.835,4.48 5.569,4.565 5.302,4.565C4.95,4.565 4.598,4.407 4.355,4.104Z"/> + <path + android:fillColor="#000000" + android:pathData="M18.161,4.262C17.627,3.838 17.542,3.073 17.955,2.552C18.379,2.03 19.132,1.945 19.666,2.358C20.187,2.77 20.272,3.534 19.86,4.068C19.617,4.359 19.265,4.517 18.913,4.517C18.646,4.517 18.379,4.432 18.161,4.262Z"/> + <path + android:fillColor="#000000" + android:pathData="M8.492,1.497C8.334,0.854 8.747,0.199 9.402,0.041C10.057,-0.105 10.7,0.308 10.858,0.963C11.003,1.606 10.591,2.261 9.948,2.407C9.851,2.431 9.754,2.443 9.669,2.443C9.123,2.443 8.613,2.067 8.492,1.497Z"/> + <path + android:fillColor="#000000" + android:pathData="M14.267,2.395C13.599,2.249 13.199,1.606 13.345,0.951C13.49,0.296 14.133,-0.116 14.788,0.029C15.443,0.175 15.856,0.83 15.71,1.485C15.589,2.043 15.08,2.431 14.534,2.431C14.437,2.431 14.352,2.419 14.267,2.395Z"/> + <path + android:fillColor="#000000" + android:pathData="M7,17.037C6.527,16.564 6.527,15.8 7,15.326C7.473,14.841 8.237,14.841 8.71,15.314C9.196,15.787 9.196,16.552 8.723,17.025C8.48,17.267 8.177,17.389 7.861,17.389C7.546,17.389 7.242,17.267 7,17.037Z"/> + <path + android:fillColor="#000000" + android:pathData="M15.565,17.012C15.092,16.539 15.092,15.762 15.565,15.289C16.038,14.816 16.814,14.816 17.288,15.289C17.761,15.762 17.761,16.539 17.288,17.012C17.045,17.243 16.742,17.364 16.426,17.364C16.111,17.364 15.807,17.243 15.565,17.012Z"/> + <path + android:fillColor="#000000" + android:pathData="M4.853,11.917C4.853,11.237 5.386,10.691 6.054,10.691C6.721,10.691 7.279,11.225 7.279,11.892C7.279,12.56 6.745,13.106 6.078,13.118C5.398,13.118 4.853,12.584 4.853,11.917Z"/> + <path + android:fillColor="#000000" + android:pathData="M16.984,11.868C16.984,11.856 16.984,11.844 16.984,11.832C16.984,11.832 16.984,11.82 16.984,11.807C16.972,11.14 17.506,10.582 18.185,10.582C18.852,10.57 19.398,11.116 19.41,11.783C19.41,11.795 19.41,11.82 19.41,11.832C19.41,11.844 19.41,11.856 19.41,11.868C19.41,12.535 18.865,13.081 18.197,13.081C17.53,13.081 16.984,12.535 16.984,11.868Z"/> + <path + android:fillColor="#000000" + android:pathData="M6.952,8.471C6.478,7.997 6.478,7.233 6.952,6.76C6.952,6.76 6.952,6.76 6.939,6.76C7.413,6.275 8.189,6.275 8.662,6.748C9.135,7.221 9.147,7.985 8.674,8.458C8.432,8.701 8.116,8.822 7.813,8.822C7.497,8.822 7.194,8.701 6.952,8.471Z"/> + <path + android:fillColor="#000000" + android:pathData="M15.529,8.399C15.043,7.938 15.043,7.161 15.504,6.688C15.977,6.203 16.742,6.203 17.227,6.664C17.7,7.137 17.712,7.901 17.239,8.387C17.009,8.629 16.693,8.751 16.378,8.751C16.075,8.751 15.759,8.629 15.529,8.399Z"/> + <path + android:fillColor="#000000" + android:pathData="M10.87,5.815C10.858,5.148 11.392,4.59 12.071,4.59C12.738,4.578 13.284,5.124 13.284,5.791C13.296,6.458 12.762,7.016 12.083,7.016C11.416,7.016 10.87,6.483 10.87,5.815Z"/> +</vector> diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt index 60983070b1cf..2a44511599f1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt @@ -40,3 +40,21 @@ class FakeNotificationsSoundPolicyRepository : NotificationsSoundPolicyRepositor mutableZenMode.value = zenMode } } + +fun FakeNotificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories: Int = 0, + priorityCallSenders: Int = NotificationManager.Policy.PRIORITY_SENDERS_ANY, + priorityMessageSenders: Int = NotificationManager.Policy.CONVERSATION_SENDERS_NONE, + suppressedVisualEffects: Int = NotificationManager.Policy.SUPPRESSED_EFFECTS_UNSET, + state: Int = NotificationManager.Policy.STATE_UNSET, + priorityConversationSenders: Int = NotificationManager.Policy.CONVERSATION_SENDERS_NONE, +) = updateNotificationPolicy( + NotificationManager.Policy( + priorityCategories, + priorityCallSenders, + priorityMessageSenders, + suppressedVisualEffects, + state, + priorityConversationSenders, + ) +)
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractor.kt new file mode 100644 index 000000000000..794cf832f48b --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractor.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.statusbar.notification.domain.interactor + +import android.app.NotificationManager +import android.media.AudioManager +import android.provider.Settings +import android.service.notification.ZenModeConfig +import com.android.settingslib.statusbar.notification.data.model.ZenMode +import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepository +import com.android.settingslib.volume.shared.model.AudioStream +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +/** Determines notification sounds state and limitations. */ +class NotificationsSoundPolicyInteractor( + private val repository: NotificationsSoundPolicyRepository +) { + + /** @see NotificationManager.getNotificationPolicy */ + val notificationPolicy: StateFlow<NotificationManager.Policy?> + get() = repository.notificationPolicy + + /** @see NotificationManager.getZenMode */ + val zenMode: StateFlow<ZenMode?> + get() = repository.zenMode + + /** Checks if [notificationPolicy] allows alarms. */ + val areAlarmsAllowed: Flow<Boolean?> = notificationPolicy.map { it?.allowAlarms() } + + /** Checks if [notificationPolicy] allows media. */ + val isMediaAllowed: Flow<Boolean?> = notificationPolicy.map { it?.allowMedia() } + + /** Checks if [notificationPolicy] allows ringer. */ + val isRingerAllowed: Flow<Boolean?> = + notificationPolicy.map { policy -> + policy ?: return@map null + !ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(policy) + } + + /** Checks if the [stream] is muted by either [zenMode] or [notificationPolicy]. */ + fun isZenMuted(stream: AudioStream): Flow<Boolean> { + return combine( + zenMode.filterNotNull(), + areAlarmsAllowed.filterNotNull(), + isMediaAllowed.filterNotNull(), + isRingerAllowed.filterNotNull(), + ) { zenMode, areAlarmsAllowed, isMediaAllowed, isRingerAllowed -> + if (zenMode.zenMode == Settings.Global.ZEN_MODE_NO_INTERRUPTIONS) { + return@combine true + } + + val isNotificationOrRing = + stream.value == AudioManager.STREAM_RING || + stream.value == AudioManager.STREAM_NOTIFICATION + if (isNotificationOrRing && zenMode.zenMode == Settings.Global.ZEN_MODE_ALARMS) { + return@combine true + } + if (zenMode.zenMode != Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) { + return@combine false + } + + if (stream.value == AudioManager.STREAM_ALARM && !areAlarmsAllowed) { + return@combine true + } + if (stream.value == AudioManager.STREAM_MUSIC && !isMediaAllowed) { + return@combine true + } + if (isNotificationOrRing && !isRingerAllowed) { + return@combine true + } + + return@combine false + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index 6851997ac323..0df4615c8b7c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -64,10 +65,10 @@ interface AudioRepository { val communicationDevice: StateFlow<AudioDeviceInfo?> /** State of the [AudioStream]. */ - suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> + fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> - /** Current state of the [AudioStream]. */ - suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel + /** Returns the last audible volume before stream was muted. */ + suspend fun getLastAudibleVolume(audioStream: AudioStream): Int suspend fun setVolume(audioStream: AudioStream, volume: Int) @@ -122,7 +123,7 @@ class AudioRepositoryImpl( audioManager.communicationDevice, ) - override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> { + override fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> { return audioManagerEventsReceiver.events .filter { if (it is StreamAudioManagerEvent) { @@ -132,20 +133,24 @@ class AudioRepositoryImpl( } } .map { getCurrentAudioStream(audioStream) } + .onStart { emit(getCurrentAudioStream(audioStream)) } .flowOn(backgroundCoroutineContext) } - override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel { + private fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel { + return AudioStreamModel( + audioStream = audioStream, + minVolume = getMinVolume(audioStream), + maxVolume = audioManager.getStreamMaxVolume(audioStream.value), + volume = audioManager.getStreamVolume(audioStream.value), + isAffectedByRingerMode = audioManager.isStreamAffectedByRingerMode(audioStream.value), + isMuted = audioManager.isStreamMute(audioStream.value), + ) + } + + override suspend fun getLastAudibleVolume(audioStream: AudioStream): Int { return withContext(backgroundCoroutineContext) { - AudioStreamModel( - audioStream = audioStream, - minVolume = getMinVolume(audioStream), - maxVolume = audioManager.getStreamMaxVolume(audioStream.value), - volume = audioManager.getStreamVolume(audioStream.value), - isAffectedByRingerMode = - audioManager.isStreamAffectedByRingerMode(audioStream.value), - isMuted = audioManager.isStreamMute(audioStream.value) - ) + audioManager.getLastAudibleStreamVolume(audioStream.value) } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt new file mode 100644 index 000000000000..56b0bf74574f --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.domain.interactor + +import android.media.AudioManager +import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor +import com.android.settingslib.volume.data.repository.AudioRepository +import com.android.settingslib.volume.shared.model.AudioStream +import com.android.settingslib.volume.shared.model.AudioStreamModel +import com.android.settingslib.volume.shared.model.RingerMode +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** Provides audio stream state and an ability to change it */ +class AudioVolumeInteractor( + private val audioRepository: AudioRepository, + private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor, +) { + + /** State of the [AudioStream]. */ + fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> = + combine( + audioRepository.getAudioStream(audioStream), + audioRepository.ringerMode, + notificationsSoundPolicyInteractor.isZenMuted(audioStream) + ) { streamModel: AudioStreamModel, ringerMode: RingerMode, isZenMuted: Boolean -> + streamModel.copy(volume = processVolume(streamModel, ringerMode, isZenMuted)) + } + + suspend fun setVolume(audioStream: AudioStream, volume: Int) = + audioRepository.setVolume(audioStream, volume) + + suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) = + audioRepository.setMuted(audioStream, isMuted) + + /** Checks if the volume can be changed via the UI. */ + fun canChangeVolume(audioStream: AudioStream): Flow<Boolean> { + return if (audioStream.value == AudioManager.STREAM_NOTIFICATION) { + getAudioStream(AudioStream(AudioManager.STREAM_RING)).map { !it.isMuted } + } else { + flowOf(true) + } + } + + private suspend fun processVolume( + audioStreamModel: AudioStreamModel, + ringerMode: RingerMode, + isZenMuted: Boolean, + ): Int { + if (isZenMuted) { + return audioRepository.getLastAudibleVolume(audioStreamModel.audioStream) + } + val isNotificationOrRing = + audioStreamModel.audioStream.value == AudioManager.STREAM_RING || + audioStreamModel.audioStream.value == AudioManager.STREAM_NOTIFICATION + if (isNotificationOrRing && ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) { + // For ringer-mode affected streams, show volume as zero when ringer mode is vibrate + if ( + audioStreamModel.audioStream.value == AudioManager.STREAM_RING || + (audioStreamModel.audioStream.value == AudioManager.STREAM_NOTIFICATION && + audioStreamModel.isMuted) + ) { + return 0 + } + } else if (audioStreamModel.isMuted) { + return 0 + } + return audioStreamModel.volume + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiver.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiver.kt index 13ed9a802318..c3b1a7cb16e3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiver.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiver.kt @@ -54,6 +54,7 @@ class AudioManagerEventsReceiverImpl( AudioManager.VOLUME_CHANGED_ACTION, AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, AudioManager.STREAM_DEVICES_CHANGED_ACTION, + AudioManager.ACTION_VOLUME_CHANGED, ) override val events: SharedFlow<AudioManagerEvent> = diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt index 58f3c2d61f3b..9c48299d81be 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt @@ -25,7 +25,7 @@ value class AudioStream(val value: Int) { require(value in supportedStreamTypes) { "Unsupported stream=$value" } } - private companion object { + companion object { val supportedStreamTypes = setOf( AudioManager.STREAM_VOICE_CALL, diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt index 9ddf876be68e..1728a8022ce5 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt @@ -181,24 +181,6 @@ class AudioRepositoryTest { } @Test - fun adjustingVolume_currentModeIsUpToDate() { - testScope.runTest { - val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) - var streamModel: AudioStreamModel? = null - underTest - .getAudioStream(audioStream) - .onEach { streamModel = it } - .launchIn(backgroundScope) - runCurrent() - - underTest.setVolume(audioStream, 50) - runCurrent() - - assertThat(underTest.getCurrentAudioStream(audioStream)).isEqualTo(streamModel) - } - } - - @Test fun muteStream_mutesTheStream() { testScope.runTest { val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index b58187d8e95e..28cdc6db192b 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -807,7 +807,9 @@ public class SettingsBackupTest { Settings.Secure.UI_TRANSLATION_ENABLED, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_EDGE_HAPTIC_ENABLED, Settings.Secure.DND_CONFIGS_MIGRATED, - Settings.Secure.NAVIGATION_MODE_RESTORE); + Settings.Secure.NAVIGATION_MODE_RESTORE, + Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, + Settings.Secure.V_TO_U_RESTORE_DENYLIST); @Test public void systemSettingsBackedUpOrDenied() { diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 71f9ba2788a1..a69a2a64dccb 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -377,6 +377,13 @@ flag { } flag { + name: "screenshot_action_dismiss_system_windows" + namespace: "systemui" + description: "Dismiss existing system windows when starting action from screenshot UI" + bug: "309933761" +} + +flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." @@ -529,3 +536,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "bind_keyguard_media_visibility" + namespace: "systemui" + description: "Binds Keyguard Media Controller Visibility to MediaContainerView" + bug: "298213983" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt index 535c2d32ed09..e862f0c43a58 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt @@ -17,7 +17,6 @@ package com.android.systemui.surfaceeffects.turbulencenoise import android.view.View import androidx.annotation.VisibleForTesting -import java.util.Random /** Plays [TurbulenceNoiseView] in ease-in, main (no easing), and ease-out order. */ class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoiseView) { @@ -37,8 +36,6 @@ class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoise } } - private val random = Random() - /** Current state of the animation. */ @VisibleForTesting var state: AnimationState = AnimationState.NOT_PLAYING @@ -95,12 +92,7 @@ class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoise } state = AnimationState.EASE_IN - // Add offset to avoid repetitive noise. - turbulenceNoiseView.playEaseIn( - offsetX = random.nextFloat(), - offsetY = random.nextFloat(), - this::playMainAnimation - ) + turbulenceNoiseView.playEaseIn(this::playMainAnimation) } private fun playMainAnimation() { diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt index c59bc106ca91..5e72e3bd1e39 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt @@ -109,7 +109,7 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex /** Plays the turbulence noise with linear ease-in. */ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) - fun playEaseIn(offsetX: Float = 0f, offsetY: Float = 0f, onAnimationEnd: Runnable? = null) { + fun playEaseIn(onAnimationEnd: Runnable? = null) { if (noiseConfig == null) { return } @@ -129,8 +129,8 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex val progress = updateListener.animatedValue as Float shader.setNoiseMove( - offsetX + initialX + timeInSec * config.noiseMoveSpeedX, - offsetY + initialY + timeInSec * config.noiseMoveSpeedY, + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, initialZ + timeInSec * config.noiseMoveSpeedZ ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt index 8bd0d45920f4..97d5b41000de 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt @@ -35,7 +35,6 @@ import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea -import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel @@ -55,7 +54,6 @@ constructor( private val vibratorHelper: VibratorHelper, private val indicationController: KeyguardIndicationController, private val indicationAreaViewModel: KeyguardIndicationAreaViewModel, - private val alphaViewModel: AodAlphaViewModel, ) { /** * Renders a single lockscreen shortcut. @@ -104,7 +102,6 @@ constructor( content { IndicationArea( indicationAreaViewModel = indicationAreaViewModel, - alphaViewModel = alphaViewModel, indicationController = indicationController, ) } @@ -183,7 +180,6 @@ constructor( @Composable private fun IndicationArea( indicationAreaViewModel: KeyguardIndicationAreaViewModel, - alphaViewModel: AodAlphaViewModel, indicationController: KeyguardIndicationController, modifier: Modifier = Modifier, ) { @@ -196,7 +192,6 @@ constructor( KeyguardIndicationAreaBinder.bind( view = view, viewModel = indicationAreaViewModel, - aodAlphaViewModel = alphaViewModel, indicationController = indicationController, ) ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 4156d833b0de..ce96d75da666 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -18,7 +18,9 @@ package com.android.systemui.communal.domain.interactor import android.app.smartspace.SmartspaceTarget +import android.appwidget.AppWidgetProviderInfo import android.content.pm.UserInfo +import android.os.UserHandle import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -51,6 +53,8 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.fakeUserTracker import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos @@ -96,6 +100,7 @@ class CommunalInteractorTest : SysuiTestCase() { private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter private lateinit var sceneInteractor: SceneInteractor + private lateinit var userTracker: FakeUserTracker private lateinit var underTest: CommunalInteractor @@ -113,6 +118,7 @@ class CommunalInteractorTest : SysuiTestCase() { editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter communalPrefsRepository = kosmos.fakeCommunalPrefsRepository sceneInteractor = kosmos.sceneInteractor + userTracker = kosmos.fakeUserTracker whenever(mainUser.isMain).thenReturn(true) whenever(secondaryUser.isMain).thenReturn(false) @@ -207,25 +213,19 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setKeyguardOccluded(false) tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - // Widgets are available. - val widgets = - listOf( - CommunalWidgetContentModel( - appWidgetId = 0, - priority = 30, - providerInfo = mock(), - ), - CommunalWidgetContentModel( - appWidgetId = 1, - priority = 20, - providerInfo = mock(), - ), - CommunalWidgetContentModel( - appWidgetId = 2, - priority = 10, - providerInfo = mock(), - ), - ) + val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK) + userRepository.setUserInfos(userInfos) + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 0, + ) + runCurrent() + + // Widgets available. + val widget1 = createWidgetForUser(1, USER_INFO_WORK.id) + val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id) + val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id) + val widgets = listOf(widget1, widget2, widget3) widgetRepository.setCommunalWidgets(widgets) val widgetContent by collectLastValue(underTest.widgetContent) @@ -752,6 +752,38 @@ class CommunalInteractorTest : SysuiTestCase() { verify(editWidgetsActivityStarter).startActivity(widgetKey) } + @Test + fun filterWidgets_whenUserProfileRemoved() = + testScope.runTest { + // Keyguard showing, and tutorial completed. + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setKeyguardOccluded(false) + tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + // Only main user exists. + val userInfos = listOf(MAIN_USER_INFO) + userRepository.setUserInfos(userInfos) + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 0, + ) + runCurrent() + + val widgetContent by collectLastValue(underTest.widgetContent) + // Given three widgets, and one of them is associated with pre-existing work profile. + val widget1 = createWidgetForUser(1, USER_INFO_WORK.id) + val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id) + val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id) + val widgets = listOf(widget1, widget2, widget3) + widgetRepository.setCommunalWidgets(widgets) + + // One widget is filtered out and the remaining two link to main user id. + assertThat(checkNotNull(widgetContent).size).isEqualTo(2) + widgetContent!!.forEachIndexed { _, model -> + assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id) + } + } + private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget { val timer = mock(SmartspaceTarget::class.java) whenever(timer.smartspaceTargetId).thenReturn(id) @@ -760,4 +792,17 @@ class CommunalInteractorTest : SysuiTestCase() { whenever(timer.creationTimeMillis).thenReturn(timestamp) return timer } + + private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel = + mock<CommunalWidgetContentModel> { + whenever(this.appWidgetId).thenReturn(appWidgetId) + val providerInfo = mock<AppWidgetProviderInfo>() + whenever(providerInfo.profile).thenReturn(UserHandle(userId)) + whenever(this.providerInfo).thenReturn(providerInfo) + } + + private companion object { + val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 352bacc56ca5..5ee88cb92fa0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -17,6 +17,9 @@ package com.android.systemui.communal.view.viewmodel import android.app.smartspace.SmartspaceTarget +import android.appwidget.AppWidgetProviderInfo +import android.content.pm.UserInfo +import android.os.UserHandle import android.provider.Settings import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -39,6 +42,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.view.MediaHost +import com.android.systemui.settings.fakeUserTracker import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos @@ -59,6 +63,7 @@ import org.mockito.MockitoAnnotations class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var providerInfo: AppWidgetProviderInfo private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -78,6 +83,11 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { widgetRepository = kosmos.fakeCommunalWidgetRepository smartspaceRepository = kosmos.fakeSmartspaceRepository mediaRepository = kosmos.fakeCommunalMediaRepository + kosmos.fakeUserTracker.set( + userInfos = listOf(MAIN_USER_INFO), + selectedUserIndex = 0, + ) + whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id)) underTest = CommunalEditModeViewModel( @@ -100,12 +110,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { CommunalWidgetContentModel( appWidgetId = 0, priority = 30, - providerInfo = mock(), + providerInfo = providerInfo, ), CommunalWidgetContentModel( appWidgetId = 1, priority = 20, - providerInfo = mock(), + providerInfo = providerInfo, ), ) widgetRepository.setCommunalWidgets(widgets) @@ -156,12 +166,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { CommunalWidgetContentModel( appWidgetId = 0, priority = 30, - providerInfo = mock(), + providerInfo = providerInfo, ), CommunalWidgetContentModel( appWidgetId = 1, priority = 20, - providerInfo = mock(), + providerInfo = providerInfo, ), ) widgetRepository.setCommunalWidgets(widgets) @@ -205,4 +215,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { underTest.onReorderWidgetCancel() verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } + + private companion object { + val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index cc322d085acd..1e523dd2a9cc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -17,7 +17,9 @@ package com.android.systemui.communal.view.viewmodel import android.app.smartspace.SmartspaceTarget +import android.appwidget.AppWidgetProviderInfo import android.content.pm.UserInfo +import android.os.UserHandle import android.provider.Settings import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -45,13 +47,13 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.view.MediaHost +import com.android.systemui.settings.fakeUserTracker import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -71,6 +73,7 @@ import org.mockito.MockitoAnnotations class CommunalViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var user: UserInfo + @Mock private lateinit var providerInfo: AppWidgetProviderInfo private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -98,6 +101,12 @@ class CommunalViewModelTest : SysuiTestCase() { kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) + kosmos.fakeUserTracker.set( + userInfos = listOf(MAIN_USER_INFO), + selectedUserIndex = 0, + ) + whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id)) + underTest = CommunalViewModel( testScope, @@ -147,12 +156,12 @@ class CommunalViewModelTest : SysuiTestCase() { CommunalWidgetContentModel( appWidgetId = 0, priority = 30, - providerInfo = mock(), + providerInfo = providerInfo, ), CommunalWidgetContentModel( appWidgetId = 1, priority = 20, - providerInfo = mock(), + providerInfo = providerInfo, ), ) widgetRepository.setCommunalWidgets(widgets) @@ -225,4 +234,8 @@ class CommunalViewModelTest : SysuiTestCase() { userRepository.setUserInfos(listOf(user)) userRepository.setSelectedUserInfo(user) } + + private companion object { + val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index 8488843905f7..2c9d72c423bc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -16,7 +16,9 @@ package com.android.systemui.communal.widgets +import android.appwidget.AppWidgetProviderInfo import android.content.pm.UserInfo +import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_COMMUNAL_HUB @@ -32,6 +34,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock @@ -65,7 +68,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) + kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO, USER_INFO_WORK)) kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) @@ -76,6 +79,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { CommunalAppWidgetHostStartable( appWidgetHost, kosmos.communalInteractor, + kosmos.fakeUserTracker, kosmos.applicationCoroutineScope, kosmos.testDispatcher, ) @@ -170,6 +174,46 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { } } + @Test + fun removeWidgetsForDeletedProfile_whenCommunalIsAvailable() = + with(kosmos) { + testScope.runTest { + // Communal is available and work profile is configured. + setCommunalAvailable(true) + kosmos.fakeUserTracker.set( + userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK), + selectedUserIndex = 0, + ) + val widget1 = createWidgetForUser(1, USER_INFO_WORK.id) + val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id) + val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id) + val widgets = listOf(widget1, widget2, widget3) + fakeCommunalWidgetRepository.setCommunalWidgets(widgets) + + underTest.start() + runCurrent() + + val communalWidgets by + collectLastValue(fakeCommunalWidgetRepository.communalWidgets) + assertThat(communalWidgets).containsExactly(widget1, widget2, widget3) + + // Unlock the device and remove work profile. + fakeKeyguardRepository.setKeyguardShowing(false) + kosmos.fakeUserTracker.set( + userInfos = listOf(MAIN_USER_INFO), + selectedUserIndex = 0, + ) + runCurrent() + + // Communal becomes available. + fakeKeyguardRepository.setKeyguardShowing(true) + runCurrent() + + // Widget created for work profile is removed. + assertThat(communalWidgets).containsExactly(widget2, widget3) + } + } + private suspend fun setCommunalAvailable(available: Boolean) = with(kosmos) { fakeKeyguardRepository.setIsEncryptedOrLockdown(false) @@ -179,7 +223,16 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, settingsValue, MAIN_USER_INFO.id) } + private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel = + mock<CommunalWidgetContentModel> { + whenever(this.appWidgetId).thenReturn(appWidgetId) + val providerInfo = mock<AppWidgetProviderInfo>() + whenever(providerInfo.profile).thenReturn(UserHandle(userId)) + whenever(this.providerInfo).thenReturn(providerInfo) + } + private companion object { val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorTest.kt new file mode 100644 index 000000000000..e188f5bfc1c8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorTest.kt @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.domain.interactor + +import android.app.NotificationManager +import android.media.AudioManager +import android.provider.Settings.Global +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.statusbar.notification.data.model.ZenMode +import com.android.settingslib.statusbar.notification.data.repository.updateNotificationPolicy +import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor +import com.android.settingslib.volume.shared.model.AudioStream +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Expect +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class NotificationsSoundPolicyInteractorTest : SysuiTestCase() { + + @JvmField @Rule val expect = Expect.create() + + private val kosmos = testKosmos() + + private lateinit var underTest: NotificationsSoundPolicyInteractor + + @Before + fun setup() { + with(kosmos) { + underTest = NotificationsSoundPolicyInteractor(notificationsSoundPolicyRepository) + } + } + + @Test + fun onlyAlarmsCategory_areAlarmsAllowed_isTrue() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateZenMode(ZenMode(Global.ZEN_MODE_OFF)) + val expectedByCategory = + NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.associateWith { + it == NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS + } + expectedByCategory.forEach { entry -> + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = entry.key + ) + + val areAlarmsAllowed by collectLastValue(underTest.areAlarmsAllowed) + runCurrent() + + expect.that(areAlarmsAllowed).isEqualTo(entry.value) + } + } + } + } + + @Test + fun onlyMediaCategory_areAlarmsAllowed_isTrue() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateZenMode(ZenMode(Global.ZEN_MODE_OFF)) + val expectedByCategory = + NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.associateWith { + it == NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA + } + expectedByCategory.forEach { entry -> + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = entry.key + ) + + val isMediaAllowed by collectLastValue(underTest.isMediaAllowed) + runCurrent() + + expect.that(isMediaAllowed).isEqualTo(entry.value) + } + } + } + } + + @Test + fun atLeastOneCategoryAllowed_isRingerAllowed_isTrue() { + with(kosmos) { + testScope.runTest { + for (category in NotificationManager.Policy.ALL_PRIORITY_CATEGORIES) { + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = category, + state = NotificationManager.Policy.STATE_UNSET, + ) + + val isRingerAllowed by collectLastValue(underTest.isRingerAllowed) + runCurrent() + + expect.that(isRingerAllowed).isTrue() + } + } + } + } + + @Test + fun allCategoriesAllowed_isRingerAllowed_isTrue() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = + NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.reduce { acc, value -> + acc or value + }, + state = NotificationManager.Policy.STATE_PRIORITY_CHANNELS_BLOCKED, + ) + + val isRingerAllowed by collectLastValue(underTest.isRingerAllowed) + runCurrent() + + assertThat(isRingerAllowed).isTrue() + } + } + } + + @Test + fun noCategoriesAndBlocked_isRingerAllowed_isFalse() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = 0, + state = NotificationManager.Policy.STATE_PRIORITY_CHANNELS_BLOCKED, + ) + + val isRingerAllowed by collectLastValue(underTest.isRingerAllowed) + runCurrent() + + assertThat(isRingerAllowed).isFalse() + } + } + } + + @Test + fun zenModeNoInterruptions_allStreams_muted() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateNotificationPolicy() + notificationsSoundPolicyRepository.updateZenMode( + ZenMode(Global.ZEN_MODE_NO_INTERRUPTIONS) + ) + + for (stream in AudioStream.supportedStreamTypes) { + val isZenMuted by collectLastValue(underTest.isZenMuted(AudioStream(stream))) + runCurrent() + + expect.that(isZenMuted).isTrue() + } + } + } + } + + @Test + fun zenModeOff_allStreams_notMuted() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateNotificationPolicy() + notificationsSoundPolicyRepository.updateZenMode(ZenMode(Global.ZEN_MODE_OFF)) + + for (stream in AudioStream.supportedStreamTypes) { + val isZenMuted by collectLastValue(underTest.isZenMuted(AudioStream(stream))) + runCurrent() + + expect.that(isZenMuted).isFalse() + } + } + } + } + + @Test + fun zenModeAlarms_ringAndNotifications_muted() { + with(kosmos) { + val expectedToBeMuted = + setOf(AudioManager.STREAM_RING, AudioManager.STREAM_NOTIFICATION) + testScope.runTest { + notificationsSoundPolicyRepository.updateNotificationPolicy() + notificationsSoundPolicyRepository.updateZenMode(ZenMode(Global.ZEN_MODE_ALARMS)) + + for (stream in AudioStream.supportedStreamTypes) { + val isZenMuted by collectLastValue(underTest.isZenMuted(AudioStream(stream))) + runCurrent() + + expect.that(isZenMuted).isEqualTo(stream in expectedToBeMuted) + } + } + } + } + + @Test + fun alarms_allowed_notMuted() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateZenMode( + ZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) + ) + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS + ) + + val isZenMuted by + collectLastValue(underTest.isZenMuted(AudioStream(AudioManager.STREAM_ALARM))) + runCurrent() + + expect.that(isZenMuted).isFalse() + } + } + } + + @Test + fun media_allowed_notMuted() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateZenMode( + ZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) + ) + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA + ) + + val isZenMuted by + collectLastValue(underTest.isZenMuted(AudioStream(AudioManager.STREAM_MUSIC))) + runCurrent() + + expect.that(isZenMuted).isFalse() + } + } + } + + @Test + fun ringer_allowed_notificationsNotMuted() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateZenMode( + ZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) + ) + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = + NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.reduce { acc, value -> + acc or value + }, + state = NotificationManager.Policy.STATE_PRIORITY_CHANNELS_BLOCKED, + ) + + val isZenMuted by + collectLastValue( + underTest.isZenMuted(AudioStream(AudioManager.STREAM_NOTIFICATION)) + ) + runCurrent() + + expect.that(isZenMuted).isFalse() + } + } + } + + @Test + fun ringer_allowed_ringNotMuted() { + with(kosmos) { + testScope.runTest { + notificationsSoundPolicyRepository.updateZenMode( + ZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) + ) + notificationsSoundPolicyRepository.updateNotificationPolicy( + priorityCategories = + NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.reduce { acc, value -> + acc or value + }, + state = NotificationManager.Policy.STATE_PRIORITY_CHANNELS_BLOCKED, + ) + + val isZenMuted by + collectLastValue(underTest.isZenMuted(AudioStream(AudioManager.STREAM_RING))) + runCurrent() + + expect.that(isZenMuted).isFalse() + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt new file mode 100644 index 000000000000..a2f3ccb8c416 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.domain.interactor + +import android.media.AudioManager +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.statusbar.notification.data.model.ZenMode +import com.android.settingslib.statusbar.notification.data.repository.updateNotificationPolicy +import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor +import com.android.settingslib.volume.shared.model.AudioStream +import com.android.settingslib.volume.shared.model.RingerMode +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.notification.domain.interactor.notificationsSoundPolicyInteractor +import com.android.systemui.statusbar.notification.domain.interactor.notificationsSoundPolicyRepository +import com.android.systemui.testKosmos +import com.android.systemui.volume.audioRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +@SmallTest +class AudioVolumeInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var underTest: AudioVolumeInteractor + + @Before + fun setup() { + with(kosmos) { + underTest = AudioVolumeInteractor(audioRepository, notificationsSoundPolicyInteractor) + + audioRepository.setRingerMode(RingerMode(AudioManager.RINGER_MODE_NORMAL)) + + notificationsSoundPolicyRepository.updateNotificationPolicy() + notificationsSoundPolicyRepository.updateZenMode(ZenMode(Settings.Global.ZEN_MODE_OFF)) + } + } + + @Test + fun setMuted_mutesStream() { + with(kosmos) { + testScope.runTest { + val model by collectLastValue(underTest.getAudioStream(audioStream)) + + underTest.setMuted(audioStream, false) + runCurrent() + assertThat(model!!.isMuted).isFalse() + + underTest.setMuted(audioStream, true) + runCurrent() + assertThat(model!!.isMuted).isTrue() + } + } + } + + @Test + fun setVolume_changesVolume() { + with(kosmos) { + testScope.runTest { + val model by collectLastValue(underTest.getAudioStream(audioStream)) + + underTest.setVolume(audioStream, 10) + runCurrent() + assertThat(model!!.volume).isEqualTo(10) + + underTest.setVolume(audioStream, 20) + runCurrent() + assertThat(model!!.volume).isEqualTo(20) + } + } + } + + @Test + fun ringMuted_notificationVolume_cantChange() { + with(kosmos) { + testScope.runTest { + val canChangeVolume by + collectLastValue( + underTest.canChangeVolume(AudioStream(AudioManager.STREAM_NOTIFICATION)) + ) + + underTest.setMuted(AudioStream(AudioManager.STREAM_RING), true) + runCurrent() + + assertThat(canChangeVolume).isFalse() + } + } + } + + @Test + fun streamIsMuted_getStream_volumeZero() { + with(kosmos) { + testScope.runTest { + val model by collectLastValue(underTest.getAudioStream(audioStream)) + + underTest.setMuted(audioStream, true) + runCurrent() + + assertThat(model!!.volume).isEqualTo(0) + } + } + } + + @Test + fun streamIsZenMuted_getStream_lastAudibleVolume() { + with(kosmos) { + testScope.runTest { + audioRepository.setLastAudibleVolume(audioStream, 30) + notificationsSoundPolicyRepository.updateZenMode( + ZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS) + ) + + val model by collectLastValue(underTest.getAudioStream(audioStream)) + runCurrent() + + assertThat(model!!.volume).isEqualTo(30) + } + } + } + + @Test + fun ringerModeVibrateAndMuted_getNotificationStream_volumeIsZero() { + with(kosmos) { + testScope.runTest { + audioRepository.setRingerMode(RingerMode(AudioManager.RINGER_MODE_VIBRATE)) + underTest.setMuted(AudioStream(AudioManager.STREAM_NOTIFICATION), true) + + val model by + collectLastValue( + underTest.getAudioStream(AudioStream(AudioManager.STREAM_NOTIFICATION)) + ) + runCurrent() + + assertThat(model!!.volume).isEqualTo(0) + } + } + } + + @Test + fun ringerModeVibrate_getRingerStream_volumeIsZero() { + with(kosmos) { + testScope.runTest { + audioRepository.setRingerMode(RingerMode(AudioManager.RINGER_MODE_VIBRATE)) + + val model by + collectLastValue( + underTest.getAudioStream(AudioStream(AudioManager.STREAM_RING)) + ) + runCurrent() + + assertThat(model!!.volume).isEqualTo(0) + } + } + } + + private companion object { + val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractorTest.kt new file mode 100644 index 000000000000..a1e4fcafd3a4 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractorTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.volume.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class VolumeSliderInteractorTest : SysuiTestCase() { + + private val underTest = VolumeSliderInteractor() + + @Test + fun translateValueToVolume() { + assertThat(underTest.translateValueToVolume(30f, volumeRange)).isEqualTo(3) + } + + @Test + fun processVolumeToValue_muted_zero() { + assertThat(underTest.processVolumeToValue(3, volumeRange, null, true)).isEqualTo(0) + } + + @Test + fun processVolumeToValue_currentValue_currentValue() { + assertThat(underTest.processVolumeToValue(3, volumeRange, 30f, false)).isEqualTo(30f) + } + + @Test + fun processVolumeToValue_currentValueDiffersVolume_returnsTranslatedVolume() { + assertThat(underTest.processVolumeToValue(1, volumeRange, 60f, false)).isEqualTo(10f) + } + + @Test + fun processVolumeToValue_currentValueDiffersNotEnoughVolume_returnsTranslatedVolume() { + assertThat(underTest.processVolumeToValue(1, volumeRange, 12f, false)).isEqualTo(12f) + } + + private companion object { + val volumeRange = 0..10 + } +} diff --git a/packages/SystemUI/res/layout/screen_share_dialog.xml b/packages/SystemUI/res/layout/screen_share_dialog.xml index 37964158a4aa..2616e8ae25e8 100644 --- a/packages/SystemUI/res/layout/screen_share_dialog.xml +++ b/packages/SystemUI/res/layout/screen_share_dialog.xml @@ -67,12 +67,12 @@ android:gravity="start"/> <!-- Buttons --> - <LinearLayout + <com.android.internal.widget.ButtonBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="@dimen/screenrecord_buttons_margin_top"> - <TextView + <Button android:id="@android:id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -83,13 +83,13 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"/> - <TextView + <Button android:id="@android:id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="0" android:text="@string/screenrecord_continue" style="@style/Widget.Dialog.Button" /> - </LinearLayout> + </com.android.internal.widget.ButtonBarLayout> </LinearLayout> </ScrollView>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 495f20f27996..53ad344189d8 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3129,6 +3129,8 @@ <!-- [CHAR LIMIT=25] Long label used by Note Task Shortcut --> <string name="note_task_shortcut_long_label">Note-taking, <xliff:g id="note_taking_app" example="Note-taking App">%1$s</xliff:g></string> + <!-- [CHAR LIMIT=NONE] Output switch chip text during broadcasting --> + <string name="audio_sharing_description">Sharing audio</string> <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is broadcasting --> <string name="broadcasting_description_is_broadcasting">Broadcasting</string> <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, title --> diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java index 00bbb20ed4f9..6af0fa069dbc 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java @@ -40,6 +40,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.MediaOutputConstants; import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.media.controls.util.MediaDataUtils; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.res.R; @@ -74,7 +75,7 @@ public class BroadcastDialogDelegate implements SystemUIDialog.Delegate { private final SystemUIDialog.Factory mSystemUIDialogFactory; private final String mCurrentBroadcastApp; private final String mOutputPackageName; - private final Executor mExecutor; + private final Executor mBgExecutor; private boolean mShouldLaunchLeBroadcastDialog; private Button mSwitchBroadcast; @@ -159,7 +160,7 @@ public class BroadcastDialogDelegate implements SystemUIDialog.Delegate { MediaOutputDialogFactory mediaOutputDialogFactory, @Nullable LocalBluetoothManager localBluetoothManager, UiEventLogger uiEventLogger, - Executor executor, + @Background Executor bgExecutor, BroadcastSender broadcastSender, SystemUIDialog.Factory systemUIDialogFactory, @Assisted(CURRENT_BROADCAST_APP) String currentBroadcastApp, @@ -171,7 +172,7 @@ public class BroadcastDialogDelegate implements SystemUIDialog.Delegate { mCurrentBroadcastApp = currentBroadcastApp; mOutputPackageName = outputPkgName; mUiEventLogger = uiEventLogger; - mExecutor = executor; + mBgExecutor = bgExecutor; mBroadcastSender = broadcastSender; if (DEBUG) { @@ -187,7 +188,7 @@ public class BroadcastDialogDelegate implements SystemUIDialog.Delegate { @Override public void onStart(SystemUIDialog dialog) { mDialogs.add(dialog); - registerBroadcastCallBack(mExecutor, mBroadcastCallback); + registerBroadcastCallBack(mBgExecutor, mBroadcastCallback); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java index 2af49cfbf1b1..b2699673f7ea 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java @@ -143,7 +143,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { int availableHeight = mTextPreview.getHeight() - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); - mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); + mTextPreview.setMaxLines(Math.max(availableHeight / mTextPreview.getLineHeight(), 1)); return true; }); super.onFinishInflate(); diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index d0044a4c029e..5397837423ff 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -29,6 +29,7 @@ import com.android.systemui.communal.shared.model.CommunalContentSize.FULL import com.android.systemui.communal.shared.model.CommunalContentSize.HALF import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter @@ -45,6 +46,7 @@ import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.settings.UserTracker import com.android.systemui.smartspace.data.repository.SmartspaceRepository import com.android.systemui.util.kotlin.BooleanFlowOperators.and import com.android.systemui.util.kotlin.BooleanFlowOperators.not @@ -59,6 +61,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf @@ -82,6 +85,7 @@ constructor( communalSettingsInteractor: CommunalSettingsInteractor, private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter, + private val userTracker: UserTracker, sceneInteractor: SceneInteractor, sceneContainerFlags: SceneContainerFlags, @CommunalLog logBuffer: LogBuffer, @@ -262,10 +266,16 @@ constructor( fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) = widgetRepository.updateWidgetOrder(widgetIdToPriorityMap) + /** All widgets present in db. */ + val communalWidgets: Flow<List<CommunalWidgetContentModel>> = + isCommunalAvailable.flatMapLatest { available -> + if (!available) emptyFlow() else widgetRepository.communalWidgets + } + /** A list of widget content to be displayed in the communal hub. */ val widgetContent: Flow<List<CommunalContentModel.Widget>> = widgetRepository.communalWidgets.map { widgets -> - widgets.map Widget@{ widget -> + filterWidgetsByExistingUsers(widgets).map Widget@{ widget -> return@Widget CommunalContentModel.Widget( appWidgetId = widget.appWidgetId, providerInfo = widget.providerInfo, @@ -345,6 +355,19 @@ constructor( return@combine ongoingContent } + /** + * Filter and retain widgets associated with an existing user, safeguarding against displaying + * stale data following user deletion. + */ + private fun filterWidgetsByExistingUsers( + list: List<CommunalWidgetContentModel>, + ): List<CommunalWidgetContentModel> { + val currentUserIds = userTracker.userProfiles.map { it.id }.toSet() + return list.filter { widget -> + currentUserIds.contains(widget.providerInfo.profile?.identifier) + } + } + companion object { /** * The user activity timeout which should be used when the communal hub is opened. A value diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt index 4ddd7681dd98..8390d62b23db 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -18,11 +18,14 @@ package com.android.systemui.communal.widgets import com.android.systemui.CoreStartable import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.UserTracker import com.android.systemui.util.kotlin.BooleanFlowOperators.or import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -37,6 +40,7 @@ class CommunalAppWidgetHostStartable constructor( private val appWidgetHost: CommunalAppWidgetHost, private val communalInteractor: CommunalInteractor, + private val userTracker: UserTracker, @Background private val bgScope: CoroutineScope, @Main private val uiDispatcher: CoroutineDispatcher ) : CoreStartable { @@ -47,6 +51,14 @@ constructor( .pairwise(false) .filter { (previous, new) -> previous != new } .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } + .sample(communalInteractor.communalWidgets, ::Pair) + .onEach { (withPrev, widgets) -> + val (_, isActive) = withPrev + // The validation is performed once the hub becomes active. + if (isActive) { + validateWidgetsAndDeleteOrphaned(widgets) + } + } .launchIn(bgScope) appWidgetHost.appWidgetIdToRemove @@ -63,4 +75,15 @@ constructor( appWidgetHost.stopListening() } } + + /** + * Ensure the existence of all associated users for widgets, and remove widgets belonging to + * users who have been deleted. + */ + private fun validateWidgetsAndDeleteOrphaned(widgets: List<CommunalWidgetContentModel>) { + val currentUserIds = userTracker.userProfiles.map { it.id }.toSet() + widgets + .filter { widget -> !currentUserIds.contains(widget.providerInfo.profile?.identifier) } + .onEach { widget -> communalInteractor.deleteWidget(id = widget.appWidgetId) } + } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 0f038e10dd4e..bc07b95c5d7f 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -36,6 +36,7 @@ import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.VibratorHelper @@ -47,16 +48,16 @@ import javax.inject.Inject @SysUISingleton class ControlActionCoordinatorImpl @Inject constructor( - private val context: Context, - private val bgExecutor: DelayableExecutor, - @Main private val uiExecutor: DelayableExecutor, - private val activityStarter: ActivityStarter, - private val broadcastSender: BroadcastSender, - private val keyguardStateController: KeyguardStateController, - private val taskViewFactory: Optional<TaskViewFactory>, - private val controlsMetricsLogger: ControlsMetricsLogger, - private val vibrator: VibratorHelper, - private val controlsSettingsRepository: ControlsSettingsRepository, + private val context: Context, + @Background private val bgExecutor: DelayableExecutor, + @Main private val uiExecutor: DelayableExecutor, + private val activityStarter: ActivityStarter, + private val broadcastSender: BroadcastSender, + private val keyguardStateController: KeyguardStateController, + private val taskViewFactory: Optional<TaskViewFactory>, + private val controlsMetricsLogger: ControlsMetricsLogger, + private val vibrator: VibratorHelper, + private val controlsSettingsRepository: ControlsSettingsRepository, ) : ControlActionCoordinator { private var dialog: Dialog? = null private var pendingAction: Action? = null diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index e8931770b15e..1157d97f2f2e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -126,6 +126,7 @@ import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.PolicyModule; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; @@ -358,6 +359,7 @@ public abstract class SystemUIModule { VisualInterruptionDecisionProvider visualInterruptionDecisionProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, + SensitiveNotificationProtectionController sensitiveNotificationProtectionController, CommonNotifCollection notifCollection, NotifPipeline notifPipeline, SysUiState sysUiState, @@ -376,6 +378,7 @@ public abstract class SystemUIModule { visualInterruptionDecisionProvider, zenModeController, notifUserManager, + sensitiveNotificationProtectionController, notifCollection, notifPipeline, sysUiState, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index e35c5a636bde..301942f6242b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -170,7 +170,6 @@ constructor( KeyguardIndicationAreaBinder.bind( notificationShadeWindowView.requireViewById(R.id.keyguard_indication_area), keyguardIndicationAreaViewModel, - aodAlphaViewModel, indicationController, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 7c1368af652c..841f52d7aa64 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -23,7 +23,7 @@ import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel +import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R @@ -51,7 +51,6 @@ object KeyguardIndicationAreaBinder { fun bind( view: ViewGroup, viewModel: KeyguardIndicationAreaViewModel, - aodAlphaViewModel: AodAlphaViewModel, indicationController: KeyguardIndicationController, ): DisposableHandle { indicationController.setIndicationArea(view) @@ -68,30 +67,10 @@ object KeyguardIndicationAreaBinder { view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { - if (keyguardBottomAreaRefactor()) { - aodAlphaViewModel.alpha.collect { alpha -> - view.apply { - this.importantForAccessibility = - if (alpha == 0f) { - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } else { - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO - } - this.alpha = alpha - } - } - } else { - viewModel.alpha.collect { alpha -> - view.apply { - this.importantForAccessibility = - if (alpha == 0f) { - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } else { - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO - } - this.alpha = alpha - } - } + // Do not independently apply alpha, as [KeyguardRootViewModel] should work + // for this and all its children + if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) { + viewModel.alpha.collect { alpha -> view.alpha = alpha } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt index 78099d9f5d90..a53c6d77d328 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt @@ -50,6 +50,15 @@ class KeyguardIndicationArea( ) } + override fun setAlpha(alpha: Float) { + super.setAlpha(alpha) + + if (alpha == 0f) { + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + } private fun indicationTopRow(): KeyguardIndicationTextView { return KeyguardIndicationTextView(context, attrs).apply { id = R.id.keyguard_indication_text diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt index ea05c1d878b8..3361343423a9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt @@ -25,7 +25,6 @@ import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea -import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.KeyguardIndicationController @@ -37,7 +36,6 @@ class DefaultIndicationAreaSection constructor( private val context: Context, private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel, - private val aodAlphaViewModel: AodAlphaViewModel, private val indicationController: KeyguardIndicationController, ) : KeyguardSection() { private val indicationAreaViewId = R.id.keyguard_indication_area @@ -56,7 +54,6 @@ constructor( KeyguardIndicationAreaBinder.bind( constraintLayout.requireViewById(R.id.keyguard_indication_area), keyguardIndicationAreaViewModel, - aodAlphaViewModel, indicationController, ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt index 42d68bab49f8..f4d70a5e78c9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt @@ -30,6 +30,8 @@ import androidx.annotation.MainThread import androidx.annotation.WorkerThread import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.flags.Flags.enableLeAudioSharing +import com.android.settingslib.flags.Flags.legacyLeAudioSharing import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice @@ -332,14 +334,28 @@ constructor( @WorkerThread private fun updateCurrent() { if (isLeAudioBroadcastEnabled()) { - current = - MediaDeviceData( - /* enabled */ true, - /* icon */ context.getDrawable(R.drawable.settings_input_antenna), - /* name */ broadcastDescription, - /* intent */ null, - /* showBroadcastButton */ showBroadcastButton = true - ) + if (enableLeAudioSharing()) { + current = + MediaDeviceData( + enabled = false, + icon = + context.getDrawable( + com.android.settingslib.R.drawable.ic_bt_le_audio_sharing + ), + name = context.getString(R.string.audio_sharing_description), + intent = null, + showBroadcastButton = false + ) + } else { + current = + MediaDeviceData( + /* enabled */ true, + /* icon */ context.getDrawable(R.drawable.settings_input_antenna), + /* name */ broadcastDescription, + /* intent */ null, + /* showBroadcastButton */ showBroadcastButton = true + ) + } } else { val aboutToConnect = aboutToConnectDeviceOverride if ( @@ -420,6 +436,7 @@ constructor( @WorkerThread private fun isLeAudioBroadcastEnabled(): Boolean { + if (!enableLeAudioSharing() && !legacyLeAudioSharing()) return false val localBluetoothManager = localBluetoothManager.get() if (localBluetoothManager != null) { val profileManager = localBluetoothManager.profileManager diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt index 9206af28eeff..ba7d41008a01 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt @@ -300,10 +300,17 @@ constructor( private fun setVisibility(view: ViewGroup?, newVisibility: Int) { val currentMediaContainer = view ?: return - val previousVisibility = currentMediaContainer.visibility - currentMediaContainer.visibility = newVisibility - if (previousVisibility != newVisibility && currentMediaContainer is MediaContainerView) { - visibilityChangedListener?.invoke(newVisibility == View.VISIBLE) + val isVisible = newVisibility == View.VISIBLE + + if (currentMediaContainer is MediaContainerView) { + val previousVisibility = currentMediaContainer.visibility + + currentMediaContainer.setKeyguardVisibility(isVisible) + if (previousVisibility != newVisibility) { + visibilityChangedListener?.invoke(isVisible) + } + } else { + currentMediaContainer.visibility = newVisibility } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index e8ad4d325591..4e940f1f84da 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -260,7 +260,6 @@ public class MediaControlPanel { private TurbulenceNoiseController mTurbulenceNoiseController; private LoadingEffect mLoadingEffect; private final GlobalSettings mGlobalSettings; - private final Random mRandom = new Random(); private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig; private boolean mWasPlaying = false; private boolean mButtonClicked = false; @@ -1294,13 +1293,14 @@ public class MediaControlPanel { mMediaViewHolder.getTurbulenceNoiseView(); int width = targetView.getWidth(); int height = targetView.getHeight(); + Random random = new Random(); return new TurbulenceNoiseAnimationConfig( /* gridCount= */ 2.14f, TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, - /* noiseOffsetX= */ mRandom.nextFloat(), - /* noiseOffsetY= */ mRandom.nextFloat(), - /* noiseOffsetZ= */ mRandom.nextFloat(), + /* noiseOffsetX= */ random.nextFloat(), + /* noiseOffsetY= */ random.nextFloat(), + /* noiseOffsetZ= */ random.nextFloat(), /* noiseMoveSpeedX= */ 0.42f, /* noiseMoveSpeedY= */ 0f, TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 416eae1b2d0c..4f062afc2af7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -72,7 +72,7 @@ open class MediaTttChipControllerReceiver @Inject constructor( context: Context, logger: MediaTttReceiverLogger, windowManager: WindowManager, - mainExecutor: DelayableExecutor, + @Main mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, dumpManager: DumpManager, diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt index 72a5c468ea94..c1b20374dbac 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt @@ -84,6 +84,10 @@ interface QSSceneAdapter { */ val qsHeight: Int + /** Compatibility for use by LockscreenShadeTransitionController. Matches default from [QS] */ + val isQsFullyCollapsed: Boolean + get() = true + sealed interface State { val isVisible: Boolean @@ -165,6 +169,10 @@ constructor( override val qsHeight: Int get() = qsImpl.value?.qsHeight ?: 0 + // If value is null, there's no QS and therefore it's fully collapsed. + override val isQsFullyCollapsed: Boolean + get() = qsImpl.value?.isFullyCollapsed ?: true + // Same config changes as in FragmentHostManager private val interestingChanges = InterestingConfigChanges( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt index bee315261f89..fb5339df7212 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt @@ -31,10 +31,13 @@ import android.view.WindowManager import android.view.WindowManagerGlobal import com.android.app.tracing.coroutines.launch import com.android.internal.infra.ServiceConnector +import com.android.systemui.Flags.screenshotActionDismissSystemWindows import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.DisplayTracker +import com.android.systemui.shared.system.ActivityManagerWrapper +import com.android.systemui.statusbar.phone.CentralSurfaces import javax.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher @@ -46,9 +49,11 @@ class ActionIntentExecutor @Inject constructor( private val context: Context, + private val activityManagerWrapper: ActivityManagerWrapper, @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, private val displayTracker: DisplayTracker, + private val keyguardController: ScreenshotKeyguardController, ) { /** * Execute the given intent with startActivity while performing operations for screenshot action @@ -74,7 +79,14 @@ constructor( user: UserHandle, overrideTransition: Boolean, ) { - dismissKeyguard() + if (screenshotActionDismissSystemWindows()) { + keyguardController.dismiss() + activityManagerWrapper.closeSystemWindows( + CentralSurfaces.SYSTEM_DIALOG_REASON_SCREENSHOT + ) + } else { + dismissKeyguard() + } if (user == myUserHandle()) { withContext(mainDispatcher) { context.startActivity(intent, options) } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotKeyguardController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotKeyguardController.kt new file mode 100644 index 000000000000..7696bbe3763e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotKeyguardController.kt @@ -0,0 +1,46 @@ +/* + * 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.screenshot + +import android.content.Context +import android.content.Intent +import com.android.internal.infra.ServiceConnector +import javax.inject.Inject +import kotlinx.coroutines.CompletableDeferred + +open class ScreenshotKeyguardController @Inject constructor(context: Context) { + private val proxyConnector: ServiceConnector<IScreenshotProxy> = + ServiceConnector.Impl( + context, + Intent(context, ScreenshotProxyService::class.java), + Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE, + context.userId, + IScreenshotProxy.Stub::asInterface + ) + + suspend fun dismiss() { + val completion = CompletableDeferred<Unit>() + val onDoneBinder = + object : IOnDoneCallback.Stub() { + override fun onDone(success: Boolean) { + completion.complete(Unit) + } + } + proxyConnector.post { it.dismissKeyguard(onDoneBinder) } + completion.await() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt index 86f652389b42..d5ab3066bfde 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt @@ -17,15 +17,16 @@ package com.android.systemui.screenshot import android.content.Intent import android.os.IBinder +import android.os.RemoteException import android.util.Log import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope +import com.android.app.tracing.coroutines.launch import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.shade.ShadeExpansionStateManager import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher -import com.android.app.tracing.coroutines.launch import kotlinx.coroutines.withContext /** Provides state from the main SystemUI process on behalf of the Screenshot process. */ @@ -56,7 +57,13 @@ constructor( private suspend fun executeAfterDismissing(callback: IOnDoneCallback) = withContext(mMainDispatcher) { activityStarter.executeRunnableDismissingKeyguard( - Runnable { callback.onDone(true) }, + { + try { + callback.onDone(true) + } catch (e: RemoteException) { + Log.w(TAG, "Failed to complete callback transaction", e) + } + }, null, true /* dismissShade */, true /* afterKeyguardGone */, diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index f2fa0ef3f30f..125f7fc0619b 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -121,7 +121,6 @@ open class UserTrackerImpl internal constructor( @GuardedBy("callbacks") private val callbacks: MutableList<DataItem> = ArrayList() - private var beforeUserSwitchingJob: Job? = null private var userSwitchingJob: Job? = null private var afterUserSwitchingJob: Job? = null @@ -194,14 +193,7 @@ open class UserTrackerImpl internal constructor( private fun registerUserSwitchObserver() { iActivityManager.registerUserSwitchObserver(object : UserSwitchObserver() { override fun onBeforeUserSwitching(newUserId: Int) { - if (isBackgroundUserSwitchEnabled) { - beforeUserSwitchingJob?.cancel() - beforeUserSwitchingJob = appScope.launch(backgroundContext) { - handleBeforeUserSwitching(newUserId) - } - } else { - handleBeforeUserSwitching(newUserId) - } + handleBeforeUserSwitching(newUserId) } override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) { @@ -233,15 +225,24 @@ open class UserTrackerImpl internal constructor( @WorkerThread protected open fun handleBeforeUserSwitching(newUserId: Int) { - Assert.isNotMainThread() setUserIdInternal(newUserId) val list = synchronized(callbacks) { callbacks.toList() } + val latch = CountDownLatch(list.size) list.forEach { - it.callback.get()?.onBeforeUserSwitching(newUserId) + val callback = it.callback.get() + if (callback != null) { + it.executor.execute { + callback.onBeforeUserSwitching(newUserId) + latch.countDown() + } + } else { + latch.countDown() + } } + latch.await() } @WorkerThread diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionController.kt index 4d0552e7cb31..adca3f2d25d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionController.kt @@ -21,9 +21,9 @@ import android.util.IndentingPrintWriter import android.util.MathUtils import androidx.annotation.FloatRange import androidx.annotation.Px -import com.android.systemui.res.R import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QS +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.SplitShadeStateController import dagger.assisted.Assisted @@ -38,7 +38,7 @@ constructor( context: Context, configurationController: ConfigurationController, dumpManager: DumpManager, - @Assisted private val qsProvider: () -> QS, + @Assisted private val qsProvider: () -> QS?, splitShadeStateController: SplitShadeStateController ) : AbstractLockscreenShadeTransitionController( @@ -48,7 +48,7 @@ constructor( splitShadeStateController ) { - private val qs: QS + private val qs: QS? get() = qsProvider() /** @@ -135,7 +135,7 @@ constructor( /* amount= */ MathUtils.saturate(qsDragDownAmount / qsSquishTransitionDistance) ) isTransitioningToFullShade = dragDownAmount > 0.0f - qs.setTransitionToFullShadeProgress( + qs?.setTransitionToFullShadeProgress( isTransitioningToFullShade, qsTransitionFraction, qsSquishTransitionFraction @@ -163,6 +163,6 @@ constructor( @AssistedFactory fun interface Factory { - fun create(qsProvider: () -> QS): LockscreenShadeQsTransitionController + fun create(qsProvider: () -> QS?): LockscreenShadeQsTransitionController } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index a59d753971f6..4ee83497b368 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -31,6 +31,7 @@ import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QS import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.res.R import com.android.systemui.shade.ShadeLockscreenInteractor import com.android.systemui.shade.data.repository.ShadeRepository @@ -84,6 +85,7 @@ constructor( private val splitShadeStateController: SplitShadeStateController, private val shadeLockscreenInteractorLazy: Lazy<ShadeLockscreenInteractor>, naturalScrollingSettingObserver: NaturalScrollingSettingObserver, + private val lazyQSSceneAdapter: Lazy<QSSceneAdapter>, ) : Dumpable { private var pulseHeight: Float = 0f @@ -93,7 +95,11 @@ constructor( private var useSplitShade: Boolean = false private lateinit var nsslController: NotificationStackScrollLayoutController lateinit var centralSurfaces: CentralSurfaces - lateinit var qS: QS + + // When in scene container mode, this will be null. In that case, we use the adapter if needed + var qS: QS? = null + private val isQsFullyCollapsed: Boolean + get() = qS?.isFullyCollapsed ?: lazyQSSceneAdapter.get().isQsFullyCollapsed /** A handler that handles the next keyguard dismiss animation. */ private var animationHandlerOnKeyguardDismiss: ((Long) -> Unit)? = null @@ -286,7 +292,8 @@ constructor( /** @return true if the interaction is accepted, false if it should be cancelled */ internal fun canDragDown(): Boolean { return (statusBarStateController.state == StatusBarState.KEYGUARD || - nsslController.isInLockedDownShade()) && (qS.isFullyCollapsed || useSplitShade) + nsslController.isInLockedDownShade()) && + (isQsFullyCollapsed || useSplitShade) } /** Called by the touch helper when when a gesture has completed all the way and released. */ @@ -410,7 +417,7 @@ constructor( get() = (statusBarStateController.getState() == StatusBarState.KEYGUARD && !keyguardBypassController.bypassEnabled && - (qS.isFullyCollapsed || useSplitShade)) + (isQsFullyCollapsed || useSplitShade)) /** The amount in pixels that the user has dragged down. */ internal var dragDownAmount = 0f diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScroller.kt b/packages/SystemUI/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScroller.kt index e47c914341a6..612a365dbe8b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScroller.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScroller.kt @@ -27,7 +27,7 @@ constructor( private val context: Context, private val scrimController: ScrimController, private val statusBarStateController: SysuiStatusBarStateController, - @Assisted private val qSProvider: () -> QS, + @Assisted private val qSProvider: () -> QS?, @Assisted private val nsslControllerProvider: () -> NotificationStackScrollLayoutController ) : LockScreenShadeOverScroller { @@ -37,7 +37,7 @@ constructor( private var maxOverScrollAmount = 0 private var previousOverscrollAmount = 0 - private val qS: QS + private val qS: QS? get() = qSProvider() private val nsslController: NotificationStackScrollLayoutController @@ -90,7 +90,7 @@ constructor( } private fun applyOverscroll(overscrollAmount: Int) { - qS.setOverScrollAmount(overscrollAmount) + qS?.setOverScrollAmount(overscrollAmount) scrimController.setNotificationsOverScrollAmount(overscrollAmount) nsslController.setOverScrollAmount(overscrollAmount) } @@ -109,7 +109,7 @@ constructor( val animator = ValueAnimator.ofInt(previousOverscrollAmount, 0) animator.addUpdateListener { val overScrollAmount = it.animatedValue as Int - qS.setOverScrollAmount(overScrollAmount) + qS?.setOverScrollAmount(overScrollAmount) scrimController.setNotificationsOverScrollAmount(overScrollAmount) nsslController.setOverScrollAmount(overScrollAmount) } @@ -143,7 +143,7 @@ constructor( @AssistedFactory fun interface Factory { fun create( - qSProvider: () -> QS, + qSProvider: () -> QS?, nsslControllerProvider: () -> NotificationStackScrollLayoutController ): SplitShadeLockScreenOverScroller } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java index 8189fe03b2ed..dfe6cd5f25b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java @@ -25,6 +25,7 @@ import androidx.annotation.VisibleForTesting; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -92,7 +93,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { @Inject public VisualStabilityCoordinator( - DelayableExecutor delayableExecutor, + @Background DelayableExecutor delayableExecutor, DumpManager dumpManager, HeadsUpManager headsUpManager, ShadeAnimationInteractor shadeAnimationInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 2a1ec3e9c64f..6548967c7462 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -16,12 +16,18 @@ package com.android.systemui.statusbar.notification.dagger; +import android.app.NotificationManager; import android.content.Context; import android.service.notification.NotificationListenerService; import com.android.internal.jank.InteractionJankMonitor; +import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepository; +import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepositoryImpl; +import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.res.R; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.notification.NotificationActivityStarter; @@ -79,13 +85,15 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter; import com.android.systemui.statusbar.policy.HeadsUpManager; +import javax.inject.Provider; + import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; - -import javax.inject.Provider; +import kotlin.coroutines.CoroutineContext; +import kotlinx.coroutines.CoroutineScope; /** * Dagger Module for classes found within the com.android.systemui.statusbar.notification package. @@ -259,4 +267,22 @@ public interface NotificationsModule { @ClassKey(VisualInterruptionDecisionProvider.class) CoreStartable startVisualInterruptionDecisionProvider( VisualInterruptionDecisionProvider provider); + + @Provides + @SysUISingleton + public static NotificationsSoundPolicyRepository provideNotificationsSoundPolicyRepository( + Context context, + NotificationManager notificationManager, + @Application CoroutineScope coroutineScope, + @Background CoroutineContext coroutineContext) { + return new NotificationsSoundPolicyRepositoryImpl(context, notificationManager, + coroutineScope, coroutineContext); + } + + @Provides + @SysUISingleton + public static NotificationsSoundPolicyInteractor provideNotificationsSoundPolicyInteractror( + NotificationsSoundPolicyRepository repository) { + return new NotificationsSoundPolicyInteractor(repository); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index 76e5fd3bd4f2..a5f42bb99e10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -45,10 +45,12 @@ import javax.inject.Inject * icons and keeping the icon assets themselves up to date as notifications change. * * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry. - * Long-term, it should probably live somewhere in the content inflation pipeline. + * Long-term, it should probably live somewhere in the content inflation pipeline. */ @SysUISingleton -class IconManager @Inject constructor( +class IconManager +@Inject +constructor( private val notifCollection: CommonNotifCollection, private val launcherApps: LauncherApps, private val iconBuilder: IconBuilder @@ -59,30 +61,30 @@ class IconManager @Inject constructor( notifCollection.addCollectionListener(entryListener) } - private val entryListener = object : NotifCollectionListener { - override fun onEntryInit(entry: NotificationEntry) { - entry.addOnSensitivityChangedListener(sensitivityListener) - } + private val entryListener = + object : NotifCollectionListener { + override fun onEntryInit(entry: NotificationEntry) { + entry.addOnSensitivityChangedListener(sensitivityListener) + } - override fun onEntryCleanUp(entry: NotificationEntry) { - entry.removeOnSensitivityChangedListener(sensitivityListener) - } + override fun onEntryCleanUp(entry: NotificationEntry) { + entry.removeOnSensitivityChangedListener(sensitivityListener) + } - override fun onRankingApplied() { - // rankings affect whether a conversation is important, which can change the icons - recalculateForImportantConversationChange() + override fun onRankingApplied() { + // rankings affect whether a conversation is important, which can change the icons + recalculateForImportantConversationChange() + } } - } - private val sensitivityListener = NotificationEntry.OnSensitivityChangedListener { - entry -> updateIconsSafe(entry) - } + private val sensitivityListener = + NotificationEntry.OnSensitivityChangedListener { entry -> updateIconsSafe(entry) } private fun recalculateForImportantConversationChange() { for (entry in notifCollection.allNotifs) { val isImportant = isImportantConversation(entry) - if (entry.icons.areIconsAvailable && - isImportant != entry.icons.isImportantConversation + if ( + entry.icons.areIconsAvailable && isImportant != entry.icons.isImportantConversation ) { updateIconsSafe(entry) } @@ -97,34 +99,35 @@ class IconManager @Inject constructor( * @throws InflationException Exception if required icons are not valid or specified */ @Throws(InflationException::class) - fun createIcons(entry: NotificationEntry) = traceSection("IconManager.createIcons") { - // Construct the status bar icon view. - val sbIcon = iconBuilder.createIconView(entry) - sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE - - // Construct the shelf icon view. - val shelfIcon = iconBuilder.createIconView(entry) - shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE - shelfIcon.visibility = View.INVISIBLE - - // Construct the aod icon view. - val aodIcon = iconBuilder.createIconView(entry) - aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE - aodIcon.setIncreasedSize(true) - - // Set the icon views' icons - val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) - - try { - setIcon(entry, normalIconDescriptor, sbIcon) - setIcon(entry, sensitiveIconDescriptor, shelfIcon) - setIcon(entry, sensitiveIconDescriptor, aodIcon) - entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, entry.icons) - } catch (e: InflationException) { - entry.icons = IconPack.buildEmptyPack(entry.icons) - throw e + fun createIcons(entry: NotificationEntry) = + traceSection("IconManager.createIcons") { + // Construct the status bar icon view. + val sbIcon = iconBuilder.createIconView(entry) + sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE + + // Construct the shelf icon view. + val shelfIcon = iconBuilder.createIconView(entry) + shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE + shelfIcon.visibility = View.INVISIBLE + + // Construct the aod icon view. + val aodIcon = iconBuilder.createIconView(entry) + aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE + aodIcon.setIncreasedSize(true) + + // Set the icon views' icons + val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) + + try { + setIcon(entry, normalIconDescriptor, sbIcon) + setIcon(entry, sensitiveIconDescriptor, shelfIcon) + setIcon(entry, sensitiveIconDescriptor, aodIcon) + entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, entry.icons) + } catch (e: InflationException) { + entry.icons = IconPack.buildEmptyPack(entry.icons) + throw e + } } - } /** * Update the notification icons. @@ -133,33 +136,33 @@ class IconManager @Inject constructor( * @throws InflationException Exception if required icons are not valid or specified */ @Throws(InflationException::class) - fun updateIcons(entry: NotificationEntry) = traceSection("IconManager.updateIcons") { - if (!entry.icons.areIconsAvailable) { - return@traceSection - } - entry.icons.smallIconDescriptor = null - entry.icons.peopleAvatarDescriptor = null + fun updateIcons(entry: NotificationEntry) = + traceSection("IconManager.updateIcons") { + if (!entry.icons.areIconsAvailable) { + return@traceSection + } + entry.icons.smallIconDescriptor = null + entry.icons.peopleAvatarDescriptor = null - val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) - val notificationContentDescription = entry.sbn.notification?.let { - iconBuilder.getIconContentDescription(it) - } + val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) + val notificationContentDescription = + entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) } - entry.icons.statusBarIcon?.let { - it.setNotification(entry.sbn, notificationContentDescription) - setIcon(entry, normalIconDescriptor, it) - } + entry.icons.statusBarIcon?.let { + it.setNotification(entry.sbn, notificationContentDescription) + setIcon(entry, normalIconDescriptor, it) + } - entry.icons.shelfIcon?.let { - it.setNotification(entry.sbn, notificationContentDescription) - setIcon(entry, normalIconDescriptor, it) - } + entry.icons.shelfIcon?.let { + it.setNotification(entry.sbn, notificationContentDescription) + setIcon(entry, sensitiveIconDescriptor, it) + } - entry.icons.aodIcon?.let { - it.setNotification(entry.sbn, notificationContentDescription) - setIcon(entry, sensitiveIconDescriptor, it) + entry.icons.aodIcon?.let { + it.setNotification(entry.sbn, notificationContentDescription) + setIcon(entry, sensitiveIconDescriptor, it) + } } - } private fun updateIconsSafe(entry: NotificationEntry) { try { @@ -173,11 +176,12 @@ class IconManager @Inject constructor( @Throws(InflationException::class) private fun getIconDescriptors(entry: NotificationEntry): Pair<StatusBarIcon, StatusBarIcon> { val iconDescriptor = getIconDescriptor(entry, redact = false) - val sensitiveDescriptor = if (entry.isSensitive) { - getIconDescriptor(entry, redact = true) - } else { - iconDescriptor - } + val sensitiveDescriptor = + if (entry.isSensitive) { + getIconDescriptor(entry, redact = true) + } else { + iconDescriptor + } return Pair(iconDescriptor, sensitiveDescriptor) } @@ -197,14 +201,15 @@ class IconManager @Inject constructor( } val icon = - (if (showPeopleAvatar) { - createPeopleAvatar(entry) - } else { - n.smallIcon - }) ?: throw InflationException( - "No icon in notification from " + entry.sbn.packageName) - - val ic = StatusBarIcon( + (if (showPeopleAvatar) { + createPeopleAvatar(entry) + } else { + n.smallIcon + }) + ?: throw InflationException("No icon in notification from " + entry.sbn.packageName) + + val ic = + StatusBarIcon( entry.sbn.user, entry.sbn.packageName, icon, @@ -282,8 +287,8 @@ class IconManager @Inject constructor( /** * Determines if this icon shows a conversation based on the sensitivity of the icon, its - * context and the user's indicated sensitivity preference. If we're using a fall back icon - * of the small icon, we don't consider this to be showing a conversation + * context and the user's indicated sensitivity preference. If we're using a fall back icon of + * the small icon, we don't consider this to be showing a conversation * * @param iconView The icon that shows the conversation. */ @@ -293,19 +298,20 @@ class IconManager @Inject constructor( iconDescriptor: StatusBarIcon ): Boolean { val usedInSensitiveContext = - iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon + iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon) - return isImportantConversation(entry) && !isSmallIcon && - (!usedInSensitiveContext || !entry.isSensitive) + return isImportantConversation(entry) && + !isSmallIcon && + (!usedInSensitiveContext || !entry.isSensitive) } private fun isImportantConversation(entry: NotificationEntry): Boolean { // Also verify that the Notification is MessagingStyle, since we're going to access // MessagingStyle-specific data (EXTRA_MESSAGES, EXTRA_MESSAGING_PERSON). return entry.ranking.channel != null && - entry.ranking.channel.isImportantConversation && - entry.sbn.notification.isStyle(MessagingStyle::class.java) && - entry.key !in unimportantConversationKeys + entry.ranking.channel.isImportantConversation && + entry.sbn.notification.isStyle(MessagingStyle::class.java) && + entry.key !in unimportantConversationKeys } override fun setUnimportantConversations(keys: Collection<String>) { @@ -323,8 +329,8 @@ private const val TAG = "IconManager" interface ConversationIconManager { /** * Sets the complete current set of notification keys which should (for the purposes of icon - * presentation) be considered unimportant. This tells the icon manager to remove the avatar - * of a group from which the priority notification has been removed. + * presentation) be considered unimportant. This tells the icon manager to remove the avatar of + * a group from which the priority notification has been removed. */ fun setUnimportantConversations(keys: Collection<String>) -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt index bae5baaf91ed..5551ab46262c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt @@ -22,6 +22,8 @@ import android.graphics.Canvas import android.graphics.Path import android.graphics.RectF import android.util.AttributeSet +import android.util.Log +import com.android.systemui.Flags import com.android.systemui.res.R import com.android.systemui.statusbar.notification.row.ExpandableView @@ -87,4 +89,63 @@ class MediaContainerView(context: Context, attrs: AttributeSet?) : ExpandableVie ) { // No animation, it doesn't need it, this would be local } + + override fun setVisibility(visibility: Int) { + if (Flags.bindKeyguardMediaVisibility()) { + if (isVisibilityValid(visibility)) { + super.setVisibility(visibility) + } + } else { + super.setVisibility(visibility) + } + + assertMediaContainerVisibility(visibility) + } + + /** + * visibility should be aligned with MediaContainerView visibility on the keyguard. + */ + private fun isVisibilityValid(visibility: Int): Boolean { + val currentViewState = viewState as? MediaContainerViewState ?: return true + val shouldBeGone = !currentViewState.shouldBeVisible + return if (shouldBeGone) visibility == GONE else visibility != GONE + } + + /** + * b/298213983 + * MediaContainerView's visibility is changed to VISIBLE when it should be GONE. + * This method check this state and logs. + */ + private fun assertMediaContainerVisibility(visibility: Int) { + val currentViewState = viewState + + if (currentViewState is MediaContainerViewState) { + if (!currentViewState.shouldBeVisible && visibility == VISIBLE) { + Log.wtf("MediaContainerView", "MediaContainerView should be GONE " + + "but its visibility changed to VISIBLE") + } + } + } + + fun setKeyguardVisibility(isVisible: Boolean) { + val currentViewState = viewState + if (currentViewState is MediaContainerViewState) { + currentViewState.shouldBeVisible = isVisible + } + + visibility = if (isVisible) VISIBLE else GONE + } + + override fun createExpandableViewState(): ExpandableViewState = MediaContainerViewState() + + class MediaContainerViewState : ExpandableViewState() { + var shouldBeVisible: Boolean = false + + override fun copyFrom(viewState: ViewState) { + super.copyFrom(viewState) + if (viewState is MediaContainerViewState) { + shouldBeVisible = viewState.shouldBeVisible + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 7925a1ce97ee..e397a70ea1f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -573,7 +573,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * Do notifications dismiss with normal transitioning */ private boolean mDismissUsingRowTranslationX = true; - private NotificationEntry mTopHeadsUpEntry; + private ExpandableNotificationRow mTopHeadsUpRow; private long mNumHeadsUp; private NotificationStackScrollLayoutController.TouchHandler mTouchHandler; private final ScreenOffAnimationController mScreenOffAnimationController; @@ -1688,10 +1688,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * is mainly used when dragging down from a heads up notification. */ private int getTopHeadsUpPinnedHeight() { - if (mTopHeadsUpEntry == null) { + if (mTopHeadsUpRow == null) { return 0; } - ExpandableNotificationRow row = mTopHeadsUpEntry.getRow(); + ExpandableNotificationRow row = mTopHeadsUpRow; if (row.isChildInGroup()) { final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(row.getEntry()); @@ -1872,8 +1872,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (slidingChild instanceof ExpandableNotificationRow row) { NotificationEntry entry = row.getEntry(); if (!mIsExpanded && row.isHeadsUp() && row.isPinned() - && mTopHeadsUpEntry.getRow() != row - && mGroupMembershipManager.getGroupSummary(mTopHeadsUpEntry) != entry) { + && mTopHeadsUpRow != row + && mGroupMembershipManager.getGroupSummary(mTopHeadsUpRow.getEntry()) + != entry) { continue; } return row.getViewAtPosition(touchY - childTop); @@ -5724,8 +5725,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mShelf.updateAppearance(); } - void setTopHeadsUpEntry(NotificationEntry topEntry) { - mTopHeadsUpEntry = topEntry; + void setTopHeadsUpRow(ExpandableNotificationRow topHeadsUpRow) { + mTopHeadsUpRow = topHeadsUpRow; } void setNumHeadsUp(long numHeadsUp) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 8dfac8617ff1..7c138776d5a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -692,7 +692,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { long numEntries = mHeadsUpManager.getAllEntries().count(); NotificationEntry topEntry = mHeadsUpManager.getTopEntry(); mView.setNumHeadsUp(numEntries); - mView.setTopHeadsUpEntry(topEntry); + mView.setTopHeadsUpRow(topEntry != null ? topEntry.getRow() : null); generateHeadsUpAnimation(entry, isHeadsUp); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 634de7a17ef7..1ef9a8f3d7ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -853,7 +853,7 @@ public class StackScrollAlgorithm { } } if (row.isHeadsUpAnimatingAway()) { - if (NotificationsImprovedHunAnimation.isEnabled()) { + if (NotificationsImprovedHunAnimation.isEnabled() && !ambientState.isDozing()) { if (shouldHunAppearFromBottom(ambientState, childState)) { // move to the bottom of the screen childState.setYTranslation( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index b772158b0825..db15144340e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1375,7 +1375,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { || !mKeyguardStateController.canDismissLockScreen() || mKeyguardViewMediator.isAnySimPinSecure() || (mQsController.getExpanded() && trackingTouch) - || mShadeSurface.getBarState() == StatusBarState.SHADE_LOCKED) { + || mShadeSurface.getBarState() == StatusBarState.SHADE_LOCKED + // This last one causes a race condition when the shade resets. Don't send a 0 + // and let StatusBarStateController process a keyguard state change instead + || 1f - fraction == 0f) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 4fd33ba458d8..5610ed926f70 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -55,6 +55,7 @@ import com.android.systemui.EventLogTags; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.power.domain.interactor.PowerInteractor; @@ -138,7 +139,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit Context context, @DisplayId int displayId, Handler mainThreadHandler, - Executor uiBgExecutor, + @Background Executor uiBgExecutor, NotificationVisibilityProvider visibilityProvider, HeadsUpManager headsUpManager, ActivityStarter activityStarter, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index f73d089c36b9..3e3ea855ccf7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -315,8 +315,8 @@ constructor( // TTL for satellite polling is one hour const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60 - // Let the system boot up (5s) and stabilize before we check for system support - const val MIN_UPTIME: Long = 1000 * 5 + // Let the system boot up and stabilize before we check for system support + const val MIN_UPTIME: Long = 1000 * 60 private const val TAG = "DeviceBasedSatelliteRepo" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt index a078dd5cf28c..2ad4d361df1f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt @@ -23,6 +23,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.net.Uri +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import com.android.systemui.util.concurrency.DelayableExecutor import javax.inject.Inject @@ -34,7 +35,7 @@ import javax.inject.Inject class BatteryStateNotifier @Inject constructor( val controller: BatteryController, val noMan: NotificationManager, - val delayableExecutor: DelayableExecutor, + @Background val delayableExecutor: DelayableExecutor, val context: Context ) : BatteryController.BatteryStateChangeCallback { var stateUnknown = false diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java index 6124f6383fff..2cad8442e3ba 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java @@ -124,15 +124,6 @@ public abstract class SysUIConcurrencyModule { } /** - * Provide a Background-Thread Executor by default. - */ - @Provides - @SysUISingleton - public static Executor provideExecutor(@Background Looper looper) { - return new ExecutorImpl(looper); - } - - /** * Provide a BroadcastRunning Executor (for sending and receiving broadcasts). */ @Provides @@ -174,15 +165,6 @@ public abstract class SysUIConcurrencyModule { } /** - * Provide a Background-Thread Executor by default. - */ - @Provides - @SysUISingleton - public static DelayableExecutor provideDelayableExecutor(@Background Looper looper) { - return new ExecutorImpl(looper); - } - - /** * Provide a Background-Thread Executor. */ @Provides @@ -193,15 +175,6 @@ public abstract class SysUIConcurrencyModule { } /** - * Provide a Background-Thread Executor by default. - */ - @Provides - @SysUISingleton - public static RepeatableExecutor provideRepeatableExecutor(@Background DelayableExecutor exec) { - return new RepeatableExecutorImpl(exec); - } - - /** * Provide a Background-Thread Executor. */ @Provides diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java index 9b72eb710588..5979f3e60cb9 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java @@ -28,6 +28,7 @@ import android.util.Log; import androidx.annotation.NonNull; import com.android.systemui.Dumpable; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.time.SystemClock; @@ -94,10 +95,12 @@ public class PersistentConnectionManager<T> implements Dumpable { } }; + // TODO: b/326449074 - Ensure the DelayableExecutor is on the correct thread, and update the + // qualifier (to @Main) or name (to bgExecutor) to be consistent with that. @Inject public PersistentConnectionManager( SystemClock clock, - DelayableExecutor mainExecutor, + @Background DelayableExecutor mainExecutor, DumpManager dumpManager, @Named(DUMPSYS_NAME) String dumpsysName, @Named(SERVICE_CONNECTION) ObservableServiceConnection<T> serviceConnection, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index f6fd519ed723..8431fbcd8bad 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -16,17 +16,16 @@ package com.android.systemui.volume.dagger -import android.app.NotificationManager import android.content.Context import android.media.AudioManager import com.android.settingslib.media.data.repository.SpatializerRepository import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl import com.android.settingslib.media.domain.interactor.SpatializerInteractor -import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepository -import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepositoryImpl +import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.data.repository.AudioRepositoryImpl import com.android.settingslib.volume.domain.interactor.AudioModeInteractor +import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.AudioManagerEventsReceiverImpl import com.android.systemui.dagger.qualifiers.Application @@ -62,6 +61,13 @@ interface AudioModule { AudioModeInteractor(repository) @Provides + fun provideAudioVolumeInteractor( + audioRepository: AudioRepository, + notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor, + ): AudioVolumeInteractor = + AudioVolumeInteractor(audioRepository, notificationsSoundPolicyInteractor) + + @Provides fun provdieSpatializerRepository( audioManager: AudioManager, @Background backgroundContext: CoroutineContext, @@ -71,19 +77,5 @@ interface AudioModule { @Provides fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor = SpatializerInteractor(repository) - - @Provides - fun provideNotificationsSoundPolicyRepository( - context: Context, - notificationManager: NotificationManager, - @Background coroutineContext: CoroutineContext, - @Application coroutineScope: CoroutineScope, - ): NotificationsSoundPolicyRepository = - NotificationsSoundPolicyRepositoryImpl( - context, - notificationManager, - coroutineScope, - coroutineContext, - ) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt index bf9963d13959..d134e60ef72f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt @@ -18,8 +18,10 @@ package com.android.systemui.volume.dagger import android.media.session.MediaSessionManager import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.MediaControllerRepository import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl +import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -44,6 +46,19 @@ interface MediaDevicesModule { @Provides @SysUISingleton + fun provideLocalMediaRepository( + factory: LocalMediaRepositoryFactory + ): LocalMediaRepository = factory.create(null) + + @Provides + @SysUISingleton + fun provideLocalMediaInteractor( + repository: LocalMediaRepository, + @Application scope: CoroutineScope, + ): LocalMediaInteractor = LocalMediaInteractor(repository, scope) + + @Provides + @SysUISingleton fun provideMediaDeviceSessionRepository( intentsReceiver: AudioManagerEventsReceiver, mediaSessionManager: MediaSessionManager, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt new file mode 100644 index 000000000000..6b62074e023d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.volume.domain.interactor + +import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor +import com.android.settingslib.volume.domain.model.RoutingSession +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Provides a remote media casting state. */ +@VolumePanelScope +class CastVolumeInteractor +@Inject +constructor( + @VolumePanelScope private val coroutineScope: CoroutineScope, + private val localMediaInteractor: LocalMediaInteractor, +) { + + /** Returns a list of [RoutingSession] to show in the UI. */ + val remoteRoutingSessions: StateFlow<List<RoutingSession>> = + localMediaInteractor.remoteRoutingSessions + .map { it.filter { routingSession -> routingSession.isVolumeSeekBarEnabled } } + .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList()) + + /** Sets [routingSession] volume to [volume]. */ + suspend fun setVolume(routingSession: RoutingSession, volume: Int) { + localMediaInteractor.adjustSessionVolume(routingSession.routingSessionInfo.id, volume) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractor.kt new file mode 100644 index 000000000000..52736c6cb08b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/VolumeSliderInteractor.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.volume.domain.interactor + +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import javax.inject.Inject + +/** Converts from slider value to volume and back. */ +@VolumePanelScope +class VolumeSliderInteractor @Inject constructor() { + + /** mimic percentage volume setting */ + private val displayValueRange: ClosedFloatingPointRange<Float> = 0f..100f + + /** + * Translates [volume], that belongs to [volumeRange] to the value that belongs to + * [displayValueRange]. + * + * [currentValue] is the raw value received from the slider. Returns [currentValue] when it + * translates to the same volume as [volume] parameter. This ensures smooth slider experience + * (avoids snapping when the user stops dragging). + */ + fun processVolumeToValue( + volume: Int, + volumeRange: ClosedRange<Int>, + currentValue: Float?, + isMuted: Boolean, + ): Float { + if (isMuted) { + return 0f + } + val changedVolume: Int? = currentValue?.let { translateValueToVolume(it, volumeRange) } + return if (volume != volumeRange.start && volume == changedVolume) { + currentValue + } else { + translateToRange( + currentValue = volume.toFloat(), + currentRangeStart = volumeRange.start.toFloat(), + currentRangeEnd = volumeRange.endInclusive.toFloat(), + targetRangeStart = displayValueRange.start, + targetRangeEnd = displayValueRange.endInclusive, + ) + } + } + + /** Translates [value] from [displayValueRange] to volume that has [volumeRange]. */ + fun translateValueToVolume( + value: Float, + volumeRange: ClosedRange<Int>, + ): Int { + return translateToRange( + currentValue = value, + currentRangeStart = displayValueRange.start, + currentRangeEnd = displayValueRange.endInclusive, + targetRangeStart = volumeRange.start.toFloat(), + targetRangeEnd = volumeRange.endInclusive.toFloat(), + ) + .toInt() + } + + /** + * Translates a value from one range to another. + * + * ``` + * Given: currentValue=3, currentRange=[0, 8], targetRange=[0, 100] + * Result: 37.5 + * ``` + */ + private fun translateToRange( + currentValue: Float, + currentRangeStart: Float, + currentRangeEnd: Float, + targetRangeStart: Float, + targetRangeEnd: Float, + ): Float { + val currentRangeLength: Float = (currentRangeEnd - currentRangeStart) + val targetRangeLength: Float = targetRangeEnd - targetRangeStart + if (currentRangeLength == 0f || targetRangeLength == 0f) { + return 0f + } + val volumeFraction: Float = (currentValue - currentRangeStart) / currentRangeLength + return targetRangeStart + volumeFraction * targetRangeLength + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/model/SliderType.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/model/SliderType.kt new file mode 100644 index 000000000000..b97123b29b68 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/model/SliderType.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.volume.domain.model + +import com.android.settingslib.volume.shared.model.AudioStream + +/** The type of volume slider that can be shown at the UI. */ +sealed interface SliderType { + + /** The slider represents one of the device volume streams. */ + data class Stream(val stream: AudioStream) : SliderType + + /** The represents media device casting volume. */ + data object MediaDeviceCast : SliderType +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index 139d190ae63c..65dede83f3d6 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -25,6 +25,7 @@ import static android.service.notification.NotificationListenerService.REASON_GR import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; +import static com.android.server.notification.Flags.screenshareNotificationHiding; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -69,6 +70,7 @@ import com.android.systemui.statusbar.notification.collection.render.Notificatio import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; import com.android.systemui.statusbar.phone.StatusBarWindowCallback; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleEntry; @@ -102,6 +104,7 @@ public class BubblesManager { private final NotificationVisibilityProvider mVisibilityProvider; private final VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider; private final NotificationLockscreenUserManager mNotifUserManager; + private final SensitiveNotificationProtectionController mSensitiveNotifProtectionController; private final CommonNotifCollection mCommonNotifCollection; private final NotifPipeline mNotifPipeline; private final NotifPipelineFlags mNotifPipelineFlags; @@ -111,6 +114,7 @@ public class BubblesManager { // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline private final List<NotifCallback> mCallbacks = new ArrayList<>(); private final StatusBarWindowCallback mStatusBarWindowCallback; + private final Runnable mSensitiveStateChangedListener; private boolean mPanelExpanded; /** @@ -130,6 +134,7 @@ public class BubblesManager { VisualInterruptionDecisionProvider visualInterruptionDecisionProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, + SensitiveNotificationProtectionController sensitiveNotificationProtectionController, CommonNotifCollection notifCollection, NotifPipeline notifPipeline, SysUiState sysUiState, @@ -149,6 +154,7 @@ public class BubblesManager { visualInterruptionDecisionProvider, zenModeController, notifUserManager, + sensitiveNotificationProtectionController, notifCollection, notifPipeline, sysUiState, @@ -173,6 +179,7 @@ public class BubblesManager { VisualInterruptionDecisionProvider visualInterruptionDecisionProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, + SensitiveNotificationProtectionController sensitiveNotificationProtectionController, CommonNotifCollection notifCollection, NotifPipeline notifPipeline, SysUiState sysUiState, @@ -188,6 +195,7 @@ public class BubblesManager { mVisibilityProvider = visibilityProvider; mVisualInterruptionDecisionProvider = visualInterruptionDecisionProvider; mNotifUserManager = notifUserManager; + mSensitiveNotifProtectionController = sensitiveNotificationProtectionController; mCommonNotifCollection = notifCollection; mNotifPipeline = notifPipeline; mNotifPipelineFlags = notifPipelineFlags; @@ -251,6 +259,22 @@ public class BubblesManager { }; notificationShadeWindowController.registerCallback(mStatusBarWindowCallback); + mSensitiveStateChangedListener = new Runnable() { + @Override + public void run() { + if (!screenshareNotificationHiding()) { + return; + } + bubbles.onSensitiveNotificationProtectionStateChanged( + mSensitiveNotifProtectionController.isSensitiveStateActive()); + } + }; + + if (screenshareNotificationHiding()) { + mSensitiveNotifProtectionController + .registerSensitiveStateListener(mSensitiveStateChangedListener); + } + mSysuiProxy = new Bubbles.SysuiProxy() { @Override public void isNotificationPanelExpand(Consumer<Boolean> callback) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt index 1205dceb49e9..711f90f043ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt @@ -23,7 +23,6 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.KeyguardIndicationController @@ -40,7 +39,6 @@ import org.mockito.MockitoAnnotations class DefaultIndicationAreaSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel - @Mock private lateinit var aodAlphaViewModel: AodAlphaViewModel @Mock private lateinit var indicationController: KeyguardIndicationController private lateinit var underTest: DefaultIndicationAreaSection @@ -52,7 +50,6 @@ class DefaultIndicationAreaSectionTest : SysuiTestCase() { DefaultIndicationAreaSection( context, keyguardIndicationAreaViewModel, - aodAlphaViewModel, indicationController, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt index 14fe18289401..7f3d79f7e288 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt @@ -27,12 +27,16 @@ import android.media.RoutingSessionInfo import android.media.session.MediaController import android.media.session.MediaController.PlaybackInfo import android.media.session.MediaSession +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.settingslib.flags.Flags import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice @@ -83,6 +87,7 @@ private const val NORMAL_APP_NAME = "NORMAL_APP_NAME" @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper public class MediaDeviceManagerTest : SysuiTestCase() { + @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() private lateinit var manager: MediaDeviceManager @Mock private lateinit var controllerFactory: MediaControllerFactory @@ -668,7 +673,28 @@ public class MediaDeviceManagerTest : SysuiTestCase() { } @Test - fun onBroadcastStarted_currentMediaDeviceDataIsBroadcasting() { + fun onBroadcastStarted_flagOff_currentMediaDeviceDataIsBroadcasting() { + mSetFlagsRule.disableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + val broadcastCallback = setupBroadcastCallback() + setupLeAudioConfiguration(true) + setupBroadcastPackage(BROADCAST_APP_NAME) + broadcastCallback.onBroadcastStarted(1, 1) + + manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() + + val data = captureDeviceData(KEY) + assertThat(data.showBroadcastButton).isFalse() + assertThat(data.enabled).isTrue() + assertThat(data.name).isEqualTo(DEVICE_NAME) + } + + @Test + @EnableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + fun onBroadcastStarted_legacy_currentMediaDeviceDataIsBroadcasting() { val broadcastCallback = setupBroadcastCallback() setupLeAudioConfiguration(true) setupBroadcastPackage(BROADCAST_APP_NAME) @@ -686,7 +712,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() { } @Test - fun onBroadcastStarted_currentMediaDeviceDataIsNotBroadcasting() { + @EnableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + fun onBroadcastStarted_legacy_currentMediaDeviceDataIsNotBroadcasting() { val broadcastCallback = setupBroadcastCallback() setupLeAudioConfiguration(true) setupBroadcastPackage(NORMAL_APP_NAME) @@ -703,6 +731,62 @@ public class MediaDeviceManagerTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + fun onBroadcastStopped_legacy_bluetoothLeBroadcastIsDisabledAndBroadcastingButtonIsGone() { + val broadcastCallback = setupBroadcastCallback() + setupLeAudioConfiguration(false) + broadcastCallback.onBroadcastStopped(1, 1) + + manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() + + val data = captureDeviceData(KEY) + assertThat(data.showBroadcastButton).isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + fun onBroadcastStarted_currentMediaDeviceDataIsBroadcasting() { + val broadcastCallback = setupBroadcastCallback() + setupLeAudioConfiguration(true) + setupBroadcastPackage(BROADCAST_APP_NAME) + broadcastCallback.onBroadcastStarted(1, 1) + + manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() + + val data = captureDeviceData(KEY) + assertThat(data.showBroadcastButton).isFalse() + assertThat(data.enabled).isFalse() + assertThat(data.name).isEqualTo(context.getString(R.string.audio_sharing_description)) + } + + @Test + @DisableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + fun onBroadcastStarted_currentMediaDeviceDataIsNotBroadcasting() { + val broadcastCallback = setupBroadcastCallback() + setupLeAudioConfiguration(true) + setupBroadcastPackage(NORMAL_APP_NAME) + broadcastCallback.onBroadcastStarted(1, 1) + + manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() + + val data = captureDeviceData(KEY) + assertThat(data.showBroadcastButton).isFalse() + assertThat(data.enabled).isFalse() + assertThat(data.name).isEqualTo(context.getString(R.string.audio_sharing_description)) + } + + @Test + @DisableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) fun onBroadcastStopped_bluetoothLeBroadcastIsDisabledAndBroadcastingButtonIsGone() { val broadcastCallback = setupBroadcastCallback() setupLeAudioConfiguration(false) @@ -714,6 +798,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { val data = captureDeviceData(KEY) assertThat(data.showBroadcastButton).isFalse() + assertThat(data.name?.equals(context.getString(R.string.audio_sharing_description))) + .isFalse() } private fun captureCallback(): LocalMediaManager.DeviceCallback { diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt new file mode 100644 index 000000000000..0c324706857f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt @@ -0,0 +1,73 @@ +/* + * 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.screenshot + +import android.content.Intent +import android.os.Process.myUserHandle +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableContext +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.DisplayTracker +import com.android.systemui.shared.system.ActivityManagerWrapper +import com.android.systemui.statusbar.phone.CentralSurfaces +import com.android.systemui.util.mockito.mock +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +class ActionIntentExecutorTest : SysuiTestCase() { + + private val scheduler = TestCoroutineScheduler() + private val mainDispatcher = StandardTestDispatcher(scheduler) + private val testScope = TestScope(mainDispatcher) + private val testableContext = TestableContext(mContext) + + private val activityManagerWrapper = mock<ActivityManagerWrapper>() + private val displayTracker = mock<DisplayTracker>() + private val keyguardController = mock<ScreenshotKeyguardController>() + + private val actionIntentExecutor = + ActionIntentExecutor( + testableContext, + activityManagerWrapper, + testScope, + mainDispatcher, + displayTracker, + keyguardController, + ) + + @Test + @EnableFlags(Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS) + fun launchIntent_callsCloseSystemWindows() = + testScope.runTest { + val intent = Intent(Intent.ACTION_EDIT).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } + val userHandle = myUserHandle() + + actionIntentExecutor.launchIntent(intent, null, userHandle, false) + scheduler.advanceUntilIdle() + + verify(activityManagerWrapper) + .closeSystemWindows(CentralSurfaces.SYSTEM_DIALOG_REASON_SCREENSHOT) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt index 032ec7440923..774aa517672e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt @@ -371,7 +371,6 @@ class UserTrackerImplTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) captor.value.onUserSwitching(newID, userSwitchingReply) assertThat(callback.calledOnUserChanging).isEqualTo(0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionControllerTest.kt index 0b4de345e2d7..402d9aab66bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionControllerTest.kt @@ -18,12 +18,13 @@ package com.android.systemui.statusbar import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QS +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController +import com.android.systemui.util.mockito.mock import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -43,13 +44,15 @@ class LockscreenShadeQsTransitionControllerTest : SysuiTestCase() { @get:Rule val expect: Expect = Expect.create() @Mock private lateinit var dumpManager: DumpManager - @Mock private lateinit var qS: QS + private var qS: QS? = null private lateinit var controller: LockscreenShadeQsTransitionController @Before fun setUp() { MockitoAnnotations.initMocks(this) + qS = mock() + setTransitionDistance(TRANSITION_DISTANCE) setTransitionDelay(TRANSITION_DELAY) setSquishTransitionDistance(SQUISH_TRANSITION_DISTANCE) @@ -220,7 +223,7 @@ class LockscreenShadeQsTransitionControllerTest : SysuiTestCase() { controller.dragDownAmount = rawDragAmount - verify(qS) + verify(qS!!) .setTransitionToFullShadeProgress( /* isTransitioningToFullShade= */ true, /* transitionFraction= */ controller.qsTransitionFraction, @@ -228,6 +231,15 @@ class LockscreenShadeQsTransitionControllerTest : SysuiTestCase() { ) } + @Test + fun nullQS_onDragAmountChanged_doesNotCrash() { + qS = null + + val rawDragAmount = 200f + + controller.dragDownAmount = rawDragAmount + } + private fun setTransitionDistance(value: Int) { overrideResource(R.dimen.lockscreen_shade_qs_transition_distance, value) configurationController.notifyConfigurationChanged() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index 91701b17b5e6..86116a073d9e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -18,6 +18,7 @@ import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingOb import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.plugins.qs.QS import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.res.R import com.android.systemui.shade.ShadeLockscreenInteractor import com.android.systemui.shade.data.repository.FakeShadeRepository @@ -82,6 +83,8 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { private val testScope get() = testComponent.testScope + private val qsSceneAdapter = FakeQSSceneAdapter({ mock() }) + lateinit var row: ExpandableNotificationRow @Mock lateinit var centralSurfaces: CentralSurfaces @@ -189,6 +192,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { splitShadeStateController = ResourcesSplitShadeStateController(), shadeLockscreenInteractorLazy = {shadeLockscreenInteractor}, naturalScrollingSettingObserver = naturalScrollingSettingObserver, + lazyQSSceneAdapter = { qsSceneAdapter } ) transitionController.addCallback(transitionControllerCallback) @@ -567,6 +571,16 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { verify(shadeLockscreenInteractor).setKeyguardStatusBarAlpha(-1f) } + @Test + fun nullQs_canDragDownFromAdapter() { + transitionController.qS = null + + qsSceneAdapter.isQsFullyCollapsed = true + assertTrue("Can't drag down on keyguard", transitionController.canDragDown()) + qsSceneAdapter.isQsFullyCollapsed = false + assertFalse("Can drag down when QS is expanded", transitionController.canDragDown()) + } + private fun enableSplitShade() { setSplitShadeEnabled(true) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt index 81d5c4d52b74..700fb1ec332c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt @@ -9,6 +9,7 @@ import com.android.systemui.plugins.qs.QS import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.android.systemui.util.mockito.mock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,7 +32,7 @@ class SplitShadeLockScreenOverScrollerTest : SysuiTestCase() { @Mock private lateinit var scrimController: ScrimController @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController - @Mock private lateinit var qS: QS + private var qS: QS? = null @Mock private lateinit var nsslController: NotificationStackScrollLayoutController @Mock private lateinit var dumpManager: DumpManager @@ -40,6 +41,7 @@ class SplitShadeLockScreenOverScrollerTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + qS = mock() whenever(nsslController.height).thenReturn(1800) @@ -92,7 +94,7 @@ class SplitShadeLockScreenOverScrollerTest : SysuiTestCase() { setDragAmount(1000f) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) setDragAmount(999f) - reset(qS, scrimController, nsslController) + reset(qS!!, scrimController, nsslController) setDragAmount(998f) setDragAmount(997f) @@ -100,8 +102,15 @@ class SplitShadeLockScreenOverScrollerTest : SysuiTestCase() { verifyNoMoreOverScrollChanges() } + @Test + fun qsNull_applyOverscroll_doesNotCrash() { + qS = null + + setDragAmount(100f) + } + private fun verifyOverScrollPerformed() { - verify(qS).setOverScrollAmount(intThat { it > 0 }) + verify(qS!!).setOverScrollAmount(intThat { it > 0 }) verify(scrimController).setNotificationsOverScrollAmount(intThat { it > 0 }) verify(nsslController).setOverScrollAmount(intThat { it > 0 }) } @@ -109,7 +118,7 @@ class SplitShadeLockScreenOverScrollerTest : SysuiTestCase() { private fun verifyOverScrollResetToZero() { // Might be more than once as the animator might have multiple values close to zero that // round down to zero. - verify(qS, atLeast(1)).setOverScrollAmount(0) + verify(qS!!, atLeast(1)).setOverScrollAmount(0) verify(scrimController, atLeast(1)).setNotificationsOverScrollAmount(0) verify(nsslController, atLeast(1)).setOverScrollAmount(0) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt index 8b99811e3d5f..a12806b9cc99 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt @@ -167,6 +167,20 @@ class IconManagerTest : SysuiTestCase() { } @Test + fun testUpdateIcons_sensitiveImportantConversation() { + val entry = + notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) + entry?.setSensitive(true, true) + entry?.channel?.isImportantConversation = true + entry?.let { iconManager.createIcons(it) } + // Updating the icons after creation shouldn't break anything + entry?.let { iconManager.updateIcons(it) } + assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc) + assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc) + assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc) + } + + @Test fun testUpdateIcons_sensitivityChange() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 91a9da399ee8..995da8192f7b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -222,6 +222,26 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_hunAnimatingAwayWhileDozing_yTranslationIsInset() { + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + + ambientState.isDozing = true + + resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset) + } + + @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) + fun resetViewStates_hunAnimatingAwayWhileDozing_hasStackMargin_changesHunYTranslation() { + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + + ambientState.isDozing = true + + resetViewStates_stackMargin_changesHunYTranslation() + } + + @Test fun resetViewStates_hunsOverlapping_bottomHunClipped() { val topHun = mockExpandableNotificationRow() val bottomHun = mockExpandableNotificationRow() diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt index 203096affd5c..08b49f026523 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt @@ -166,4 +166,28 @@ class TurbulenceNoiseControllerTest : SysuiTestCase() { assertThat(config.color).isEqualTo(expectedColor) } } + + @Test + fun play_initializesShader() { + val expectedNoiseOffset = floatArrayOf(0.1f, 0.2f, 0.3f) + val config = + TurbulenceNoiseAnimationConfig( + noiseOffsetX = expectedNoiseOffset[0], + noiseOffsetY = expectedNoiseOffset[1], + noiseOffsetZ = expectedNoiseOffset[2] + ) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) + + fakeExecutor.execute { + turbulenceNoiseController.play(SIMPLEX_NOISE, config) + + assertThat(turbulenceNoiseView.noiseConfig).isNotNull() + val shader = turbulenceNoiseView.turbulenceNoiseShader!! + assertThat(shader).isNotNull() + assertThat(shader.noiseOffsetX).isEqualTo(expectedNoiseOffset[0]) + assertThat(shader.noiseOffsetY).isEqualTo(expectedNoiseOffset[1]) + assertThat(shader.noiseOffsetZ).isEqualTo(expectedNoiseOffset[2]) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 373d209426d4..d2e03861b022 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -801,8 +801,9 @@ public class VolumeDialogImplTest extends SysuiTestCase { Log.d(TAG, "teardown: entered"); setOrientation(mOriginalOrientation); Log.d(TAG, "teardown: after setOrientation"); - mAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration); - Log.d(TAG, "teardown: after advanceTimeBy"); + // Unclear why we used to do this, and it seems to be a source of flakes + // mAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration); + Log.d(TAG, "teardown: skipped advanceTimeBy"); mTestableLooper.moveTimeForward(mLongestHideShowAnimationDuration); Log.d(TAG, "teardown: after moveTimeForward"); mTestableLooper.processAllMessages(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index b25ac24093c5..a9308601a314 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -26,6 +26,7 @@ import static android.service.notification.NotificationListenerService.REASON_AP import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING; import static com.google.common.truth.Truth.assertThat; @@ -46,6 +47,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static kotlinx.coroutines.flow.FlowKt.emptyFlow; @@ -73,6 +75,8 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.service.dreams.IDreamManager; import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; @@ -161,6 +165,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; @@ -257,6 +262,8 @@ public class BubblesTest extends SysuiTestCase { private NotificationShadeWindowView mNotificationShadeWindowView; @Mock private AuthController mAuthController; + @Mock + private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController; private SysUiState mSysUiState; private boolean mSysUiStateBubblesExpanded; @@ -272,6 +279,8 @@ public class BubblesTest extends SysuiTestCase { private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverArgumentCaptor; @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallbackCaptor; + @Captor + private ArgumentCaptor<Runnable> mSensitiveStateChangedListener; private BubblesManager mBubblesManager; private TestableBubbleController mBubbleController; @@ -594,6 +603,7 @@ public class BubblesTest extends SysuiTestCase { interruptionDecisionProvider, mZenModeController, mLockscreenUserManager, + mSensitiveNotificationProtectionController, mCommonNotifCollection, mNotifPipeline, mSysUiState, @@ -2203,6 +2213,33 @@ public class BubblesTest extends SysuiTestCase { assertThat(mBubbleController.getLayerView().isExpanded()).isFalse(); } + @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Test + public void doesNotRegisterSensitiveStateListener() { + verifyZeroInteractions(mSensitiveNotificationProtectionController); + } + + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Test + public void registerSensitiveStateListener() { + verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any()); + } + + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Test + public void onSensitiveNotificationProtectionStateChanged() { + verify(mSensitiveNotificationProtectionController, atLeastOnce()) + .registerSensitiveStateListener(mSensitiveStateChangedListener.capture()); + + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + mSensitiveStateChangedListener.getValue().run(); + verify(mBubbleController).onSensitiveNotificationProtectionStateChanged(true); + + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + mSensitiveStateChangedListener.getValue().run(); + verify(mBubbleController).onSensitiveNotificationProtectionStateChanged(false); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index 6af08d3df554..f74cf71f9e7b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.settings.userTracker import com.android.systemui.smartspace.data.repository.smartspaceRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock @@ -46,6 +47,7 @@ val Kosmos.communalInteractor by Fixture { appWidgetHost = mock(), keyguardInteractor = keyguardInteractor, editWidgetsActivityStarter = editWidgetsActivityStarter, + userTracker = userTracker, logBuffer = logcatLogBuffer("CommunalInteractor"), tableLogBuffer = mock(), communalSettingsInteractor = communalSettingsInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt index b1581d1771fd..4d902fa35204 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt @@ -41,6 +41,8 @@ class FakeQSSceneAdapter( private val _navBarPadding = MutableStateFlow<Int>(0) val navBarPadding = _navBarPadding.asStateFlow() + override var isQsFullyCollapsed: Boolean = true + override suspend fun inflate(context: Context) { _view.value = inflateDelegate(context) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterKosmos.kt new file mode 100644 index 000000000000..00ab0b57fab7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.adapter + +import android.view.View +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.fakeQSSceneAdapter by Kosmos.Fixture { FakeQSSceneAdapter({ mock<View>() }) } + +val Kosmos.qsSceneAdapter: QSSceneAdapter by Kosmos.Fixture { fakeQSSceneAdapter } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt index e5072f1c9f1c..e4a3896378f6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt @@ -26,6 +26,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.media.controls.ui.controller.mediaHierarchyManager import com.android.systemui.plugins.activityStarter +import com.android.systemui.qs.ui.adapter.qsSceneAdapter import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeLockscreenInteractor @@ -61,5 +62,6 @@ val Kosmos.lockscreenShadeTransitionController by Fixture { splitShadeStateController = splitShadeStateController, shadeLockscreenInteractorLazy = { shadeLockscreenInteractor }, naturalScrollingSettingObserver = naturalScrollingSettingObserver, + lazyQSSceneAdapter = { qsSceneAdapter } ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorKosmos.kt new file mode 100644 index 000000000000..0614309a3910 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorKosmos.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.domain.interactor + +import com.android.settingslib.statusbar.notification.data.repository.FakeNotificationsSoundPolicyRepository +import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor +import com.android.systemui.kosmos.Kosmos + +var Kosmos.notificationsSoundPolicyRepository by + Kosmos.Fixture { FakeNotificationsSoundPolicyRepository() } + +val Kosmos.notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor by + Kosmos.Fixture { NotificationsSoundPolicyInteractor(notificationsSoundPolicyRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt index fed3e171862d..a3ad2b87d5f5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt @@ -42,6 +42,7 @@ class FakeAudioRepository : AudioRepository { get() = mutableCommunicationDevice.asStateFlow() private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf() + private val lastAudibleVolumes: MutableMap<AudioStream, Int> = mutableMapOf() private fun getAudioStreamModelState( audioStream: AudioStream @@ -59,12 +60,9 @@ class FakeAudioRepository : AudioRepository { ) } - override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> = + override fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> = getAudioStreamModelState(audioStream).asStateFlow() - override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel = - getAudioStreamModelState(audioStream).value - override suspend fun setVolume(audioStream: AudioStream, volume: Int) { getAudioStreamModelState(audioStream).update { it.copy(volume = volume) } } @@ -73,6 +71,9 @@ class FakeAudioRepository : AudioRepository { getAudioStreamModelState(audioStream).update { it.copy(isMuted = isMuted) } } + override suspend fun getLastAudibleVolume(audioStream: AudioStream): Int = + lastAudibleVolumes.getOrDefault(audioStream, 0) + fun setMode(newMode: Int) { mutableMode.value = newMode } @@ -88,4 +89,8 @@ class FakeAudioRepository : AudioRepository { fun setAudioStreamModel(model: AudioStreamModel) { getAudioStreamModelState(model.audioStream).update { model } } + + fun setLastAudibleVolume(audioStream: AudioStream, volume: Int) { + lastAudibleVolumes[audioStream] = volume + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt index 7835fc89ea52..284bd55f15d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt @@ -27,20 +27,19 @@ class FakeLocalMediaRepository : LocalMediaRepository { private val volumeBySession: MutableMap<String?, Int> = mutableMapOf() - private val mutableMediaDevices = MutableStateFlow<Collection<MediaDevice>>(emptyList()) - override val mediaDevices: StateFlow<Collection<MediaDevice>> + private val mutableMediaDevices = MutableStateFlow<List<MediaDevice>>(emptyList()) + override val mediaDevices: StateFlow<List<MediaDevice>> get() = mutableMediaDevices.asStateFlow() private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null) override val currentConnectedDevice: StateFlow<MediaDevice?> get() = mutableCurrentConnectedDevice.asStateFlow() - private val mutableRemoteRoutingSessions = - MutableStateFlow<Collection<RoutingSession>>(emptyList()) - override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> + private val mutableRemoteRoutingSessions = MutableStateFlow<List<RoutingSession>>(emptyList()) + override val remoteRoutingSessions: StateFlow<List<RoutingSession>> get() = mutableRemoteRoutingSessions.asStateFlow() - fun updateMediaDevices(devices: Collection<MediaDevice>) { + fun updateMediaDevices(devices: List<MediaDevice>) { mutableMediaDevices.value = devices } diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index 4a4c29030f3c..eb3c55cb4ff6 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -255,6 +255,7 @@ android.view.Display android.view.Display$HdrCapabilities android.view.Display$Mode android.view.DisplayInfo +android.view.inputmethod.InputBinding android.hardware.SerialManager android.hardware.SerialManagerInternal diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java index 9b27dd347caf..40b6ff01965e 100644 --- a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java @@ -232,9 +232,63 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { } /** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */ - private static boolean isBrailleDisplay(byte[] descriptor) { - // TODO: b/316036493 - Check that descriptor includes 0x41 reports. - return true; + @VisibleForTesting + static boolean isBrailleDisplay(byte[] descriptor) { + boolean foundMatch = false; + for (int i = 0; i < descriptor.length; i++) { + // HID Spec "6.2.2.2 Short Items" defines that the report descriptor is a collection of + // items: each item is a collection of bytes where the first byte defines info about + // the type of item and the following 0, 1, 2, or 4 bytes are data bytes for that item. + // All items in the HID descriptor are expected to be Short Items. + final byte itemInfo = descriptor[i]; + if (!isHidItemShort(itemInfo)) { + Slog.w(LOG_TAG, "Item " + itemInfo + " declares unsupported long type"); + return false; + } + final int dataSize = getHidItemDataSize(itemInfo); + if (i + dataSize >= descriptor.length) { + Slog.w(LOG_TAG, "Item " + itemInfo + " specifies size past the remaining bytes"); + return false; + } + // The item we're looking for (usage page declaration) should have size 1. + if (dataSize == 1) { + final byte itemData = descriptor[i + 1]; + if (isHidItemBrailleDisplayUsagePage(itemInfo, itemData)) { + foundMatch = true; + } + } + // Move to the next item by skipping past all data bytes in this item. + i += dataSize; + } + return foundMatch; + } + + private static boolean isHidItemShort(byte itemInfo) { + // Info bits 7-4 describe the item type, and HID Spec "6.2.2.3 Long Items" says that long + // items always have type bits 1111. Otherwise, the item is a short item. + return (itemInfo & 0b1111_0000) != 0b1111_0000; + } + + private static int getHidItemDataSize(byte itemInfo) { + // HID Spec "6.2.2.2 Short Items" says that info bits 0-1 specify the optional data size: + // 0, 1, 2, or 4 bytes. + return switch (itemInfo & 0b0000_0011) { + case 0b00 -> 0; + case 0b01 -> 1; + case 0b10 -> 2; + default -> 4; + }; + } + + private static boolean isHidItemBrailleDisplayUsagePage(byte itemInfo, byte itemData) { + // From HID Spec "6.2.2.7 Global Items" + final byte usagePageType = 0b0000_0100; + // From HID Usage Tables version 1.2. + final byte brailleDisplayUsagePage = 0x41; + // HID Spec "6.2.2.2 Short Items" says item info bits 2-7 describe the type and + // function of the item. + final byte itemType = (byte) (itemInfo & 0b1111_1100); + return itemType == usagePageType && itemData == brailleDisplayUsagePage; } /** diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig index 71f2b9e8e10b..e9f959f4b9eb 100644 --- a/services/backup/flags.aconfig +++ b/services/backup/flags.aconfig @@ -35,6 +35,15 @@ flag { } flag { + name: "enable_v_to_u_restore_for_system_components_in_allowlist" + namespace: "onboarding" + description: "Enables system components to opt in to support restore in V to U downgrade " + "scenario without opting in for restoreAnyVersion." + bug: "324233962" + is_fixed_read_only: true +} + +flag { name: "enable_increase_datatypes_for_agent_logging" namespace: "onboarding" description: "Increase the number of a supported datatypes that an agent can define for its " diff --git a/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java b/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java index 9f0deea503cf..6e98e68601f6 100644 --- a/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java +++ b/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java @@ -177,6 +177,10 @@ public class PackageManagerBackupAgent extends BackupAgent { return mHasMetadata; } + public int getSourceSdk() { + return mStoredSdkVersion; + } + public Metadata getRestoredMetadata(String packageName) { if (mRestoredSignatures == null) { Slog.w(TAG, "getRestoredMetadata() before metadata read!"); diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index d85dd879e21d..e666442af9c9 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -44,6 +44,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManagerInternal; +import android.os.Build; import android.os.Bundle; import android.os.Message; import android.os.ParcelFileDescriptor; @@ -51,6 +52,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.provider.Settings; import android.util.EventLog; import android.util.Slog; @@ -82,6 +84,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Set; @@ -158,6 +161,12 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // When finished call listener private final OnTaskFinishedListener mListener; + // List of packages that support V-> U downgrade but do not have RestoreAnyVersion set to true. + private List<String> mVToUAllowlist; + + // List of packages that have RestoreAnyVersion set to true but do not support V-> U downgrade. + private List<String> mVToUDenylist; + // Key/value: bookkeeping about staged data and files for agent access private File mBackupDataName; private File mStageName; @@ -172,7 +181,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { @VisibleForTesting PerformUnifiedRestoreTask( UserBackupManagerService backupManagerService, - TransportConnection transportConnection) { + TransportConnection transportConnection, + String vToUAllowlist, String vToUDenyList) { mListener = null; mAgentTimeoutParameters = null; mOperationStorage = null; @@ -183,6 +193,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { mBackupEligibilityRules = null; this.backupManagerService = backupManagerService; mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(/* monitor= */ null); + mVToUAllowlist = createVToUList(vToUAllowlist); + mVToUDenylist = createVToUList(vToUDenyList); } // This task can assume that the wakelock is properly held for it and doesn't have to worry @@ -223,6 +235,18 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { backupManagerService.getAgentTimeoutParameters(), "Timeout parameters cannot be null"); mBackupEligibilityRules = backupEligibilityRules; + mVToUAllowlist = + createVToUList( + Settings.Secure.getStringForUser( + backupManagerService.getContext().getContentResolver(), + Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, + mUserId)); + mVToUDenylist = + createVToUList( + Settings.Secure.getStringForUser( + backupManagerService.getContext().getContentResolver(), + Settings.Secure.V_TO_U_RESTORE_DENYLIST, + mUserId)); if (targetPackage != null) { // Single package restore @@ -636,60 +660,29 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // Data is from a "newer" version of the app than we have currently // installed. If the app has not declared that it is prepared to // handle this case, we do not attempt the restore. - if ((mCurrentPackage.applicationInfo.flags - & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) - == 0) { - String message = - "Source version " - + metaInfo.versionCode - + " > installed version " - + mCurrentPackage.getLongVersionCode(); - Slog.w(TAG, "Package " + pkgName + ": " + message); - Bundle monitoringExtras = - mBackupManagerMonitorEventSender.putMonitoringExtra( - null, - BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION, - metaInfo.versionCode); - monitoringExtras = - mBackupManagerMonitorEventSender.putMonitoringExtra( - monitoringExtras, - BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, - false); - monitoringExtras = addRestoreOperationTypeToEvent(monitoringExtras); - mBackupManagerMonitorEventSender.monitorEvent( - BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER, - mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - monitoringExtras); - EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName, message); - nextState = UnifiedRestoreState.RUNNING_QUEUE; - return; + if (mIsSystemRestore + && isVToUDowngrade(mPmAgent.getSourceSdk(), android.os.Build.VERSION.SDK_INT)) { + if (isPackageEligibleForVToURestore(mCurrentPackage)) { + Slog.i(TAG, "Package " + pkgName + + " is eligible for V to U downgrade scenario"); + } else { + String message = "Package not eligible for V to U downgrade scenario"; + Slog.i(TAG, pkgName + " : " + message); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName, message); + nextState = UnifiedRestoreState.RUNNING_QUEUE; + return; + } } else { - if (DEBUG) { - Slog.v( - TAG, - "Source version " - + metaInfo.versionCode - + " > installed version " - + mCurrentPackage.getLongVersionCode() - + " but restoreAnyVersion"); + if ((mCurrentPackage.applicationInfo.flags + & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) + == 0) { + // Downgrade scenario with RestoreAnyVersion flag off + logDowngradeScenario(/* isRestoreAnyVersion */ false, metaInfo); + nextState = UnifiedRestoreState.RUNNING_QUEUE; + return; + } else { + logDowngradeScenario(/* isRestoreAnyVersion */ true, metaInfo); } - Bundle monitoringExtras = - mBackupManagerMonitorEventSender.putMonitoringExtra( - null, - BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION, - metaInfo.versionCode); - monitoringExtras = - mBackupManagerMonitorEventSender.putMonitoringExtra( - monitoringExtras, - BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, - true); - monitoringExtras = addRestoreOperationTypeToEvent(monitoringExtras); - mBackupManagerMonitorEventSender.monitorEvent( - BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER, - mCurrentPackage, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - monitoringExtras); } } @@ -1673,4 +1666,86 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { return mBackupManagerMonitorEventSender.putMonitoringExtra( extras, BackupManagerMonitor.EXTRA_LOG_OPERATION_TYPE, RESTORE); } + + // checks the sdk of the target/source device for a B&R operation. + // system components can opt in/out of V->U restore via allowlists. All other apps are + // not impacted + @SuppressWarnings("AndroidFrameworkCompatChange") + @VisibleForTesting + protected boolean isVToUDowngrade(int sourceSdk, int targetSdk) { + // We assume that if the source sdk is greater than U then the source is V. + return Flags.enableVToURestoreForSystemComponentsInAllowlist() + && (sourceSdk > Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + && (targetSdk == Build.VERSION_CODES.UPSIDE_DOWN_CAKE); + } + + @VisibleForTesting + protected List<String> createVToUList(@Nullable String listString) { + // The allowlist/denylist is stored as a comma-separated list of package names + List<String> list = new ArrayList<>(); + if (listString != null) { + list = Arrays.asList(listString.split(",")); + } + return list; + } + + @VisibleForTesting + protected boolean isPackageEligibleForVToURestore(PackageInfo mCurrentPackage) { + // A package is eligible for V to U downgrade restore if either: + // - The package has restoreAnyVersion set to false and is part of the V to U allowlist + // (and not in the denylist) + // - The package has restoreAnyVersion set to true and is not part of the denylist + if (mVToUDenylist.contains(mCurrentPackage.packageName)){ + return false; + } else if ((mCurrentPackage.applicationInfo.flags + & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) + == 0) { + // package has restoreAnyVersion set to false + return mVToUAllowlist.contains(mCurrentPackage.packageName); + } else { + // package has restoreAnyVersion set to true and is nor in denylist + return true; + } + } + + private void logDowngradeScenario(boolean isRestoreAnyVersion, Metadata metaInfo) { + Bundle monitoringExtras = + mBackupManagerMonitorEventSender.putMonitoringExtra( + null, + BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION, + metaInfo.versionCode); + String message; + if (isRestoreAnyVersion) { + monitoringExtras = + mBackupManagerMonitorEventSender.putMonitoringExtra( + monitoringExtras, + BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, + true); + message = "Source version " + + metaInfo.versionCode + + " > installed version " + + mCurrentPackage.getLongVersionCode() + + " but restoreAnyVersion"; + } else { + monitoringExtras = + mBackupManagerMonitorEventSender.putMonitoringExtra( + monitoringExtras, + BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, + false); + message = "Source version " + + metaInfo.versionCode + + " > installed version " + + mCurrentPackage.getLongVersionCode(); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, mCurrentPackage.packageName, + message); + } + Slog.i(TAG, "Package " + mCurrentPackage.packageName + ": " + message); + monitoringExtras = addRestoreOperationTypeToEvent(monitoringExtras); + mBackupManagerMonitorEventSender.monitorEvent( + BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER, + mCurrentPackage, + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + monitoringExtras); + } + } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 767f54d6a8c7..966fe5bbeecc 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -120,6 +120,7 @@ public class SettingsToPropertiesMapper { static final String[] sDeviceConfigAconfigScopes = new String[] { "accessibility", "android_core_networking", + "android_stylus", "aoc", "app_widgets", "arc_next", diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java index bba5ba35dbc7..631e7518b746 100644 --- a/services/core/java/com/android/server/display/BrightnessThrottler.java +++ b/services/core/java/com/android/server/display/BrightnessThrottler.java @@ -37,9 +37,11 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; +import com.android.server.display.config.SensorData; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.utils.DebugUtils; import com.android.server.display.utils.DeviceConfigParsingUtils; +import com.android.server.display.utils.SensorUtils; import java.io.PrintWriter; import java.util.HashMap; @@ -79,7 +81,7 @@ class BrightnessThrottler { // Maps the throttling ID to the data. Sourced from DisplayDeviceConfig. @NonNull - private HashMap<String, ThermalBrightnessThrottlingData> mDdcThermalThrottlingDataMap; + private Map<String, ThermalBrightnessThrottlingData> mDdcThermalThrottlingDataMap; // Current throttling data being used. // Null if we do not support throttling. @@ -97,6 +99,10 @@ class BrightnessThrottler { // The brightness throttling configuration that should be used. private String mThermalBrightnessThrottlingDataId; + // Temperature Sensor to be monitored for throttling. + @NonNull + private SensorData mTempSensor; + // This is a collection of brightness throttling data that has been written as overrides from // the DeviceConfig. This will always take priority over the display device config data. // We need to store the data for every display device, so we do not need to update this each @@ -121,17 +127,19 @@ class BrightnessThrottler { BrightnessThrottler(Handler handler, Runnable throttlingChangeCallback, String uniqueDisplayId, String throttlingDataId, - @NonNull HashMap<String, ThermalBrightnessThrottlingData> - thermalBrightnessThrottlingDataMap) { - this(new Injector(), handler, handler, throttlingChangeCallback, - uniqueDisplayId, throttlingDataId, thermalBrightnessThrottlingDataMap); + @NonNull DisplayDeviceConfig displayDeviceConfig) { + this(new Injector(), handler, handler, throttlingChangeCallback, uniqueDisplayId, + throttlingDataId, + displayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(), + displayDeviceConfig.getTempSensor()); } @VisibleForTesting BrightnessThrottler(Injector injector, Handler handler, Handler deviceConfigHandler, Runnable throttlingChangeCallback, String uniqueDisplayId, String throttlingDataId, - @NonNull HashMap<String, ThermalBrightnessThrottlingData> - thermalBrightnessThrottlingDataMap) { + @NonNull Map<String, ThermalBrightnessThrottlingData> + thermalBrightnessThrottlingDataMap, + @NonNull SensorData tempSensor) { mInjector = injector; mHandler = handler; @@ -147,7 +155,7 @@ class BrightnessThrottler { mDdcThermalThrottlingDataMap = thermalBrightnessThrottlingDataMap; loadThermalBrightnessThrottlingDataFromDeviceConfig(); loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(mDdcThermalThrottlingDataMap, - mThermalBrightnessThrottlingDataId, mUniqueDisplayId); + tempSensor, mThermalBrightnessThrottlingDataId, mUniqueDisplayId); } boolean deviceSupportsThrottling() { @@ -180,12 +188,14 @@ class BrightnessThrottler { } void loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig( - HashMap<String, ThermalBrightnessThrottlingData> ddcThrottlingDataMap, + Map<String, ThermalBrightnessThrottlingData> ddcThrottlingDataMap, + SensorData tempSensor, String brightnessThrottlingDataId, String uniqueDisplayId) { mDdcThermalThrottlingDataMap = ddcThrottlingDataMap; mThermalBrightnessThrottlingDataId = brightnessThrottlingDataId; mUniqueDisplayId = uniqueDisplayId; + mTempSensor = tempSensor; resetThermalThrottlingData(); } @@ -310,7 +320,7 @@ class BrightnessThrottler { } if (deviceSupportsThrottling()) { - mSkinThermalStatusObserver.startObserving(); + mSkinThermalStatusObserver.startObserving(mTempSensor); } } @@ -357,6 +367,7 @@ class BrightnessThrottler { private final class SkinThermalStatusObserver extends IThermalEventListener.Stub { private final Injector mInjector; private final Handler mHandler; + private SensorData mObserverTempSensor; private IThermalService mThermalService; private boolean mStarted; @@ -371,28 +382,51 @@ class BrightnessThrottler { if (DEBUG) { Slog.d(TAG, "New thermal throttling status = " + temp.getStatus()); } + + if (mObserverTempSensor.name != null + && !mObserverTempSensor.name.equals(temp.getName())) { + Slog.i(TAG, "Skipping thermal throttling notification as monitored sensor: " + + mObserverTempSensor.name + + " != notified sensor: " + + temp.getName()); + return; + } mHandler.post(() -> { final @Temperature.ThrottlingStatus int status = temp.getStatus(); thermalStatusChanged(status); }); } - void startObserving() { - if (mStarted) { + void startObserving(SensorData tempSensor) { + if (!mStarted || mObserverTempSensor == null) { + mObserverTempSensor = tempSensor; + registerThermalListener(); + return; + } + + String curType = mObserverTempSensor.type; + mObserverTempSensor = tempSensor; + if (curType.equals(tempSensor.type)) { if (DEBUG) { Slog.d(TAG, "Thermal status observer already started"); } return; } + stopObserving(); + registerThermalListener(); + } + + void registerThermalListener() { mThermalService = mInjector.getThermalService(); if (mThermalService == null) { Slog.e(TAG, "Could not observe thermal status. Service not available"); return; } + int temperatureType = SensorUtils.getSensorTemperatureType(mObserverTempSensor); try { // We get a callback immediately upon registering so there's no need to query // for the current value. - mThermalService.registerThermalEventListenerWithType(this, Temperature.TYPE_SKIN); + mThermalService.registerThermalEventListenerWithType(this, temperatureType); mStarted = true; } catch (RemoteException e) { Slog.e(TAG, "Failed to register thermal status listener", e); @@ -418,6 +452,7 @@ class BrightnessThrottler { void dump(PrintWriter writer) { writer.println(" SkinThermalStatusObserver:"); writer.println(" mStarted: " + mStarted); + writer.println(" mObserverTempSensor: " + mObserverTempSensor); if (mThermalService != null) { writer.println(" ThermalService available"); } else { diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index d1374a5ab9dc..9b2dcc53f456 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -384,6 +384,10 @@ import javax.xml.datatype.DatatypeConfigurationException; * </point> * </supportedModes> * </proxSensor> + * <tempSensor> + * <type>DISPLAY</type> + * <name>VIRTUAL-SKIN-DISPLAY</name> + * </tempSensor> * * <ambientLightHorizonLong>10001</ambientLightHorizonLong> * <ambientLightHorizonShort>2001</ambientLightHorizonShort> @@ -625,6 +629,12 @@ public class DisplayDeviceConfig { @Nullable private SensorData mProximitySensor; + // The details of the temperature sensor associated with this display. + // Throttling will be based on thermal status of this sensor. + // For empty values default back to sensor of TYPE_SKIN. + @NonNull + private SensorData mTempSensor; + private final List<RefreshRateLimitation> mRefreshRateLimitations = new ArrayList<>(2 /*initialCapacity*/); @@ -821,10 +831,10 @@ public class DisplayDeviceConfig { private String mLowBlockingZoneThermalMapId = null; private String mHighBlockingZoneThermalMapId = null; - private final HashMap<String, ThermalBrightnessThrottlingData> + private final Map<String, ThermalBrightnessThrottlingData> mThermalBrightnessThrottlingDataMapByThrottlingId = new HashMap<>(); - private final HashMap<String, PowerThrottlingData> + private final Map<String, PowerThrottlingData> mPowerThrottlingDataMapByThrottlingId = new HashMap<>(); private final Map<String, SparseArray<SurfaceControl.RefreshRateRange>> @@ -1489,6 +1499,13 @@ public class DisplayDeviceConfig { return mProximitySensor; } + /** + * @return temperature sensor data associated with the display. + */ + public SensorData getTempSensor() { + return mTempSensor; + } + boolean isAutoBrightnessAvailable() { return mAutoBrightnessAvailable; } @@ -1539,7 +1556,7 @@ public class DisplayDeviceConfig { /** * @return brightness throttling configuration data for this display, for each throttling id. */ - public HashMap<String, ThermalBrightnessThrottlingData> + public Map<String, ThermalBrightnessThrottlingData> getThermalBrightnessThrottlingDataMapByThrottlingId() { return mThermalBrightnessThrottlingDataMapByThrottlingId; } @@ -1558,7 +1575,7 @@ public class DisplayDeviceConfig { /** * @return power throttling configuration data for this display, for each throttling id. **/ - public HashMap<String, PowerThrottlingData> + public Map<String, PowerThrottlingData> getPowerThrottlingDataMapByThrottlingId() { return mPowerThrottlingDataMapByThrottlingId; } @@ -1871,6 +1888,7 @@ public class DisplayDeviceConfig { + "mAmbientLightSensor=" + mAmbientLightSensor + ", mScreenOffBrightnessSensor=" + mScreenOffBrightnessSensor + ", mProximitySensor=" + mProximitySensor + + ", mTempSensor=" + mTempSensor + ", mRefreshRateLimitations= " + Arrays.toString(mRefreshRateLimitations.toArray()) + ", mDensityMapping= " + mDensityMapping + ", mAutoBrightnessBrighteningLightDebounce= " @@ -1972,6 +1990,7 @@ public class DisplayDeviceConfig { mContext.getResources()); mScreenOffBrightnessSensor = SensorData.loadScreenOffBrightnessSensorConfig(config); mProximitySensor = SensorData.loadProxSensorConfig(config); + mTempSensor = SensorData.loadTempSensorConfig(mFlags, config); loadAmbientHorizonFromDdc(config); loadBrightnessChangeThresholds(config); loadAutoBrightnessConfigValues(config); @@ -1999,6 +2018,7 @@ public class DisplayDeviceConfig { loadBrightnessRampsFromConfigXml(); mAmbientLightSensor = SensorData.loadAmbientLightSensorConfig(mContext.getResources()); mProximitySensor = SensorData.loadSensorUnspecifiedConfig(); + mTempSensor = SensorData.loadTempSensorUnspecifiedConfig(); loadBrightnessChangeThresholdsFromXml(); loadAutoBrightnessConfigsFromConfigXml(); loadAutoBrightnessAvailableFromConfigXml(); @@ -2026,6 +2046,7 @@ public class DisplayDeviceConfig { setSimpleMappingStrategyValues(); mAmbientLightSensor = SensorData.loadAmbientLightSensorConfig(mContext.getResources()); mProximitySensor = SensorData.loadSensorUnspecifiedConfig(); + mTempSensor = SensorData.loadTempSensorUnspecifiedConfig(); loadAutoBrightnessAvailableFromConfigXml(); } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index d5863a73a0c3..3965d55b0c28 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -861,6 +861,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId; mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig( config.getThermalBrightnessThrottlingDataMapByThrottlingId(), + config.getTempSensor(), mThermalBrightnessThrottlingDataId, mUniqueDisplayId); } @@ -923,6 +924,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig); mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig( mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(), + mDisplayDeviceConfig.getTempSensor(), mThermalBrightnessThrottlingDataId, mUniqueDisplayId); } @@ -1996,7 +1998,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call postBrightnessChangeRunnable(); }, mUniqueDisplayId, mLogicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId, - ddConfig.getThermalBrightnessThrottlingDataMapByThrottlingId()); + ddConfig); } private void blockScreenOn() { diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index bc5fcb449c95..18e8fab54e3e 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -38,6 +38,7 @@ import com.android.server.display.DisplayDeviceConfig.PowerThrottlingConfigData; import com.android.server.display.DisplayDeviceConfig.PowerThrottlingData; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.config.SensorData; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.feature.DisplayManagerFlags; @@ -336,5 +337,10 @@ public class BrightnessClamperController { public float getBrightnessWearBedtimeModeCap() { return mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(); } + + @NonNull + public SensorData getTempSensor() { + return mDisplayDeviceConfig.getTempSensor(); + } } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java index 944a8a65693b..449825831182 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java @@ -35,8 +35,10 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; +import com.android.server.display.config.SensorData; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.utils.DeviceConfigParsingUtils; +import com.android.server.display.utils.SensorUtils; import java.io.PrintWriter; import java.util.List; @@ -49,9 +51,8 @@ class BrightnessThermalClamper extends BrightnessClamper<BrightnessThermalClamper.ThermalData> { private static final String TAG = "BrightnessThermalClamper"; - - @Nullable - private final IThermalService mThermalService; + @NonNull + private final ThermalStatusObserver mThermalStatusObserver; @NonNull private final DeviceConfigParameterProvider mConfigParameterProvider; // data from DeviceConfig, for all displays, for all dataSets @@ -66,7 +67,6 @@ class BrightnessThermalClamper extends // otherwise mDataFromDeviceConfig @Nullable private ThermalBrightnessThrottlingData mThermalThrottlingDataActive = null; - private boolean mStarted = false; @Nullable private String mUniqueDisplayId = null; @Nullable @@ -74,14 +74,6 @@ class BrightnessThermalClamper extends @Temperature.ThrottlingStatus private int mThrottlingStatus = Temperature.THROTTLING_NONE; - private final IThermalEventListener mThermalEventListener = new IThermalEventListener.Stub() { - @Override - public void notifyThrottling(Temperature temperature) { - @Temperature.ThrottlingStatus int status = temperature.getStatus(); - mHandler.post(() -> thermalStatusChanged(status)); - } - }; - private final BiFunction<String, String, ThrottlingLevel> mDataPointMapper = (key, value) -> { try { int status = DeviceConfigParsingUtils.parseThermalStatus(key); @@ -105,12 +97,11 @@ class BrightnessThermalClamper extends BrightnessThermalClamper(Injector injector, Handler handler, ClamperChangeListener listener, ThermalData thermalData) { super(handler, listener); - mThermalService = injector.getThermalService(); mConfigParameterProvider = injector.getDeviceConfigParameterProvider(); + mThermalStatusObserver = new ThermalStatusObserver(injector, handler); mHandler.post(() -> { setDisplayData(thermalData); loadOverrideData(); - start(); }); } @@ -139,32 +130,19 @@ class BrightnessThermalClamper extends @Override void stop() { - if (!mStarted) { - return; - } - try { - mThermalService.unregisterThermalEventListener(mThermalEventListener); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to unregister thermal status listener", e); - } - mStarted = false; + mThermalStatusObserver.stopObserving(); } @Override void dump(PrintWriter writer) { writer.println("BrightnessThermalClamper:"); - writer.println(" mStarted: " + mStarted); - if (mThermalService != null) { - writer.println(" ThermalService available"); - } else { - writer.println(" ThermalService not available"); - } writer.println(" mThrottlingStatus: " + mThrottlingStatus); writer.println(" mUniqueDisplayId: " + mUniqueDisplayId); writer.println(" mDataId: " + mDataId); writer.println(" mDataOverride: " + mThermalThrottlingDataOverride); writer.println(" mDataFromDeviceConfig: " + mThermalThrottlingDataFromDeviceConfig); writer.println(" mDataActive: " + mThermalThrottlingDataActive); + mThermalStatusObserver.dump(writer); super.dump(writer); } @@ -193,6 +171,7 @@ class BrightnessThermalClamper extends Slog.wtf(TAG, "Thermal throttling data is missing for thermalThrottlingDataId=" + mDataId); } + mThermalStatusObserver.registerSensor(data.getTempSensor()); } private void recalculateBrightnessCap() { @@ -226,19 +205,91 @@ class BrightnessThermalClamper extends } } - private void start() { - if (mThermalService == null) { - Slog.e(TAG, "Could not observe thermal status. Service not available"); - return; + + private final class ThermalStatusObserver extends IThermalEventListener.Stub { + private final Injector mInjector; + private final Handler mHandler; + private IThermalService mThermalService; + private boolean mStarted; + private SensorData mObserverTempSensor; + + ThermalStatusObserver(Injector injector, Handler handler) { + mInjector = injector; + mHandler = handler; + mStarted = false; } - try { - // We get a callback immediately upon registering so there's no need to query - // for the current value. - mThermalService.registerThermalEventListenerWithType(mThermalEventListener, - Temperature.TYPE_SKIN); - mStarted = true; - } catch (RemoteException e) { - Slog.e(TAG, "Failed to register thermal status listener", e); + + void registerSensor(SensorData tempSensor) { + if (!mStarted || mObserverTempSensor == null) { + mObserverTempSensor = tempSensor; + registerThermalListener(); + return; + } + + String curType = mObserverTempSensor.type; + mObserverTempSensor = tempSensor; + if (curType.equals(tempSensor.type)) { + Slog.d(TAG, "Thermal status observer already started"); + return; + } + stopObserving(); + registerThermalListener(); + } + + void registerThermalListener() { + mThermalService = mInjector.getThermalService(); + if (mThermalService == null) { + Slog.e(TAG, "Could not observe thermal status. Service not available"); + return; + } + int temperatureType = SensorUtils.getSensorTemperatureType(mObserverTempSensor); + try { + // We get a callback immediately upon registering so there's no need to query + // for the current value. + mThermalService.registerThermalEventListenerWithType(this, temperatureType); + mStarted = true; + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register thermal status listener", e); + } + } + + @Override + public void notifyThrottling(Temperature temp) { + Slog.d(TAG, "New thermal throttling status = " + temp.getStatus()); + if (mObserverTempSensor.name != null + && !mObserverTempSensor.name.equals(temp.getName())) { + Slog.i(TAG, "Skipping thermal throttling notification as monitored sensor: " + + mObserverTempSensor.name + + " != notified sensor: " + + temp.getName()); + return; + } + @Temperature.ThrottlingStatus int status = temp.getStatus(); + mHandler.post(() -> thermalStatusChanged(status)); + } + + void stopObserving() { + if (!mStarted) { + return; + } + try { + mThermalService.unregisterThermalEventListener(this); + mStarted = false; + } catch (RemoteException e) { + Slog.e(TAG, "Failed to unregister thermal status listener", e); + } + mThermalService = null; + } + + void dump(PrintWriter writer) { + writer.println(" ThermalStatusObserver:"); + writer.println(" mStarted: " + mStarted); + writer.println(" mObserverTempSensor: " + mObserverTempSensor); + if (mThermalService != null) { + writer.println(" ThermalService available"); + } else { + writer.println(" ThermalService not available"); + } } } @@ -251,6 +302,9 @@ class BrightnessThermalClamper extends @Nullable ThermalBrightnessThrottlingData getThermalBrightnessThrottlingData(); + + @NonNull + SensorData getTempSensor(); } @VisibleForTesting diff --git a/services/core/java/com/android/server/display/config/SensorData.java b/services/core/java/com/android/server/display/config/SensorData.java index 3bb35bf7c49f..8e716f8380b6 100644 --- a/services/core/java/com/android/server/display/config/SensorData.java +++ b/services/core/java/com/android/server/display/config/SensorData.java @@ -22,6 +22,7 @@ import android.content.res.Resources; import android.text.TextUtils; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.feature.DisplayManagerFlags; import java.util.ArrayList; import java.util.Collections; @@ -32,6 +33,9 @@ import java.util.List; */ public class SensorData { + public static final String TEMPERATURE_TYPE_DISPLAY = "DISPLAY"; + public static final String TEMPERATURE_TYPE_SKIN = "SKIN"; + @Nullable public final String type; @Nullable @@ -143,6 +147,32 @@ public class SensorData { } /** + * Loads temperature sensor data for no config case. (Type: SKIN, Name: null) + */ + public static SensorData loadTempSensorUnspecifiedConfig() { + return new SensorData(TEMPERATURE_TYPE_SKIN, null); + } + + /** + * Loads temperature sensor data from given display config. + * If empty or null config given default to (Type: SKIN, Name: null) + */ + public static SensorData loadTempSensorConfig(DisplayManagerFlags flags, + DisplayConfiguration config) { + SensorDetails sensorDetails = config.getTempSensor(); + if (!flags.isSensorBasedBrightnessThrottlingEnabled() || sensorDetails == null) { + return new SensorData(TEMPERATURE_TYPE_SKIN, null); + } + String name = sensorDetails.getName(); + String type = sensorDetails.getType(); + if (TextUtils.isEmpty(type) || TextUtils.isEmpty(name)) { + type = TEMPERATURE_TYPE_SKIN; + name = null; + } + return new SensorData(type, name); + } + + /** * Loads sensor unspecified config, this means system should use default sensor. * See also {@link com.android.server.display.utils.SensorUtils} */ diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index 1ae255933f66..516d4b1d4125 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -121,6 +121,11 @@ public class DisplayManagerFlags { Flags::refreshRateVotingTelemetry ); + private final FlagState mSensorBasedBrightnessThrottling = new FlagState( + Flags.FLAG_SENSOR_BASED_BRIGHTNESS_THROTTLING, + Flags::sensorBasedBrightnessThrottling + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -247,6 +252,10 @@ public class DisplayManagerFlags { return mRefreshRateVotingTelemetry.isEnabled(); } + public boolean isSensorBasedBrightnessThrottlingEnabled() { + return mSensorBasedBrightnessThrottling.isEnabled(); + } + /** * dumps all flagstates * @param pw printWriter @@ -270,6 +279,7 @@ public class DisplayManagerFlags { pw.println(" " + mAutoBrightnessModesFlagState); pw.println(" " + mFastHdrTransitions); pw.println(" " + mRefreshRateVotingTelemetry); + pw.println(" " + mSensorBasedBrightnessThrottling); } private static class FlagState { diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index c2f52b5ad8a0..63ab3a95822f 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -184,3 +184,11 @@ flag { bug: "310029108" is_fixed_read_only: true } + +flag { + name: "sensor_based_brightness_throttling" + namespace: "display_manager" + description: "Feature flag for enabling brightness throttling using sensor from config." + bug: "294900859" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/display/utils/SensorUtils.java b/services/core/java/com/android/server/display/utils/SensorUtils.java index 8b9fe1083187..c63473a4b3d7 100644 --- a/services/core/java/com/android/server/display/utils/SensorUtils.java +++ b/services/core/java/com/android/server/display/utils/SensorUtils.java @@ -16,9 +16,11 @@ package com.android.server.display.utils; +import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.Sensor; import android.hardware.SensorManager; +import android.os.Temperature; import android.text.TextUtils; import com.android.server.display.config.SensorData; @@ -70,4 +72,17 @@ public class SensorUtils { return null; } + /** + * Convert string temperature type to its corresponding integer value. + */ + public static int getSensorTemperatureType(@NonNull SensorData tempSensor) { + if (tempSensor.type.equalsIgnoreCase(SensorData.TEMPERATURE_TYPE_DISPLAY)) { + return Temperature.TYPE_DISPLAY; + } else if (tempSensor.type.equalsIgnoreCase(SensorData.TEMPERATURE_TYPE_SKIN)) { + return Temperature.TYPE_SKIN; + } + throw new IllegalArgumentException( + "tempSensor doesn't support type: " + tempSensor.type); + } + } diff --git a/services/core/java/com/android/server/input/debug/FocusEventDebugView.java b/services/core/java/com/android/server/input/debug/FocusEventDebugView.java index 3ffd2e1dec71..b30f5ecb2dd5 100644 --- a/services/core/java/com/android/server/input/debug/FocusEventDebugView.java +++ b/services/core/java/com/android/server/input/debug/FocusEventDebugView.java @@ -313,7 +313,7 @@ public class FocusEventDebugView extends RelativeLayout { case KeyEvent.KEYCODE_FORWARD_DEL: return "\u2326"; case KeyEvent.KEYCODE_ESCAPE: - return "ESC"; + return "esc"; case KeyEvent.KEYCODE_DPAD_UP: return "\u2191"; case KeyEvent.KEYCODE_DPAD_DOWN: @@ -330,6 +330,14 @@ public class FocusEventDebugView extends RelativeLayout { return "\u2198"; case KeyEvent.KEYCODE_DPAD_DOWN_LEFT: return "\u2199"; + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + return "\u23ef"; + case KeyEvent.KEYCODE_HOME: + return "\u25ef"; + case KeyEvent.KEYCODE_BACK: + return "\u25c1"; + case KeyEvent.KEYCODE_RECENT_APPS: + return "\u25a1"; default: break; } diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java index 84a59b4d28e4..7251ac42c582 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java @@ -43,6 +43,9 @@ import com.android.internal.inputmethod.InputBindResult; * the given {@link Handler} thread if {@link IInputMethodClient} is not a proxy object. Be careful * about its call ordering characteristics.</p> */ +// TODO(b/322895594) Mark this class to be host side test compatible once enabling fw/services in +// Ravenwood (mark this class with @RavenwoodKeepWholeClass and #create with @RavenwoodReplace, +// so Ravenwood can properly swap create method during test execution). final class IInputMethodClientInvoker { private static final String TAG = InputMethodManagerService.TAG; private static final boolean DEBUG = InputMethodManagerService.DEBUG; @@ -64,6 +67,16 @@ final class IInputMethodClientInvoker { return new IInputMethodClientInvoker(inputMethodClient, isProxy, isProxy ? null : handler); } + @AnyThread + @Nullable + static IInputMethodClientInvoker create$ravenwood( + @Nullable IInputMethodClient inputMethodClient, @NonNull Handler handler) { + if (inputMethodClient == null) { + return null; + } + return new IInputMethodClientInvoker(inputMethodClient, true, null); + } + private IInputMethodClientInvoker(@NonNull IInputMethodClient target, boolean isProxy, @Nullable Handler handler) { mTarget = target; diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java index db70ce281eb5..a110e5637f82 100644 --- a/services/core/java/com/android/server/media/MediaSession2Record.java +++ b/services/core/java/com/android/server/media/MediaSession2Record.java @@ -40,6 +40,7 @@ public class MediaSession2Record implements MediaSessionRecordImpl { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final Object mLock = new Object(); + private final int mUniqueId; @GuardedBy("mLock") private final Session2Token mSessionToken; @GuardedBy("mLock") @@ -63,11 +64,13 @@ public class MediaSession2Record implements MediaSessionRecordImpl { MediaSessionService service, Looper handlerLooper, int pid, - int policies) { + int policies, + int uniqueId) { // The lock is required to prevent `Controller2Callback` from using partially initialized // `MediaSession2Record.this`. synchronized (mLock) { mSessionToken = sessionToken; + mUniqueId = uniqueId; mService = service; mHandlerExecutor = new HandlerExecutor(new Handler(handlerLooper)); mController = new MediaController2.Builder(service.getContext(), sessionToken) @@ -98,6 +101,13 @@ public class MediaSession2Record implements MediaSessionRecordImpl { } @Override + public int getUniqueId() { + synchronized (mLock) { + return mUniqueId; + } + } + + @Override public String getPackageName() { return mSessionToken.getPackageName(); } @@ -200,6 +210,7 @@ public class MediaSession2Record implements MediaSessionRecordImpl { @Override public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "uniqueId=" + mUniqueId); pw.println(prefix + "token=" + mSessionToken); pw.println(prefix + "controller=" + mController); @@ -209,8 +220,7 @@ public class MediaSession2Record implements MediaSessionRecordImpl { @Override public String toString() { - // TODO(jaewan): Also add getId(). - return getPackageName() + " (userId=" + getUserId() + ")"; + return getPackageName() + "/" + mUniqueId + " (userId=" + getUserId() + ")"; } private class Controller2Callback extends MediaController2.ControllerCallback { diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 53f780e4d19e..15527041d8eb 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -173,6 +173,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR private final int mUserId; private final String mPackageName; private final String mTag; + private final int mUniqueId; private final Bundle mSessionInfo; private final ControllerStub mController; private final MediaSession.Token mSessionToken; @@ -223,15 +224,25 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR private int mPolicies; - public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName, - ISessionCallback cb, String tag, Bundle sessionInfo, - MediaSessionService service, Looper handlerLooper, int policies) + public MediaSessionRecord( + int ownerPid, + int ownerUid, + int userId, + String ownerPackageName, + ISessionCallback cb, + String tag, + int uniqueId, + Bundle sessionInfo, + MediaSessionService service, + Looper handlerLooper, + int policies) throws RemoteException { mOwnerPid = ownerPid; mOwnerUid = ownerUid; mUserId = userId; mPackageName = ownerPackageName; mTag = tag; + mUniqueId = uniqueId; mSessionInfo = sessionInfo; mController = new ControllerStub(); mSessionToken = new MediaSession.Token(ownerUid, mController); @@ -292,6 +303,16 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR } /** + * Get the unique id of this session record. + * + * @return a unique id of this session record. + */ + @Override + public int getUniqueId() { + return mUniqueId; + } + + /** * Get the info for this session. * * @return Info that identifies this session. @@ -703,7 +724,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR @Override public String toString() { - return mPackageName + "/" + mTag + " (userId=" + mUserId + ")"; + return mPackageName + "/" + mTag + "/" + mUniqueId + " (userId=" + mUserId + ")"; } @Override diff --git a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java index 99c8ea93936e..e53a2dbe8101 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java +++ b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java @@ -32,6 +32,13 @@ import java.io.PrintWriter; public interface MediaSessionRecordImpl extends AutoCloseable { /** + * Get the unique id of this session record. + * + * @return a unique id of this session record. + */ + int getUniqueId(); + + /** * Get the info for this session. * * @return Info that identifies this session. diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 757b26c45ab1..9e98a5809650 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -106,6 +106,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; /** * System implementation of MediaSessionManager @@ -155,6 +156,8 @@ public class MediaSessionService extends SystemService implements Monitor { /* Maps uid with all user engaging session tokens associated to it */ private final SparseArray<Set<MediaSession.Token>> mUserEngagingSessions = new SparseArray<>(); + private final AtomicInteger mNextMediaSessionRecordId = new AtomicInteger(1); + // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile) // It's always not null after the MediaSessionService is started. private FullUserRecord mCurrentFullUserRecord; @@ -193,7 +196,8 @@ public class MediaSessionService extends SystemService implements Monitor { MediaSessionService.this, mRecordThread.getLooper(), pid, - /* policies= */ 0); + /* policies= */ 0, + /* uniqueId= */ mNextMediaSessionRecordId.getAndIncrement()); synchronized (mLock) { FullUserRecord user = getFullUserRecordLocked(record.getUserId()); if (user != null) { @@ -794,9 +798,19 @@ public class MediaSessionService extends SystemService implements Monitor { final MediaSessionRecord session; try { - session = new MediaSessionRecord(callerPid, callerUid, userId, - callerPackageName, cb, tag, sessionInfo, this, - mRecordThread.getLooper(), policies); + session = + new MediaSessionRecord( + callerPid, + callerUid, + userId, + callerPackageName, + cb, + tag, + /* uniqueId= */ mNextMediaSessionRecordId.getAndIncrement(), + sessionInfo, + this, + mRecordThread.getLooper(), + policies); } catch (RemoteException e) { throw new RuntimeException("Media Session owner died prematurely.", e); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index fc7b87317344..3a7ac0bd659d 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -8206,7 +8206,7 @@ public class NotificationManagerService extends SystemService { try { return mTelecomManager.isInManagedCall() || mTelecomManager.isInSelfManagedCall(pkg, - UserHandle.getUserHandleForUid(uid), /* hasCrossUserAccess */ true); + /* hasCrossUserAccess */ true); } catch (IllegalStateException ise) { // Telecom is not ready (this is likely early boot), so there are no calls. return false; diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 9b347d572459..bf06c2aba405 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -15430,17 +15430,18 @@ public class BatteryStatsImpl extends BatteryStats { mPerDisplayBatteryStats[i].screenStateAtLastEnergyMeasurement = screenState; } - final boolean compatibleConfig; if (supportedStandardBuckets != null) { final EnergyConsumerStats.Config config = new EnergyConsumerStats.Config( supportedStandardBuckets, customBucketNames, SUPPORTED_PER_PROCESS_STATE_STANDARD_ENERGY_BUCKETS, getBatteryConsumerProcessStateNames()); - if (mEnergyConsumerStatsConfig == null) { - compatibleConfig = true; - } else { - compatibleConfig = mEnergyConsumerStatsConfig.isCompatible(config); + if (mEnergyConsumerStatsConfig != null + && !mEnergyConsumerStatsConfig.isCompatible(config)) { + // Supported power buckets changed since last boot. + // Existing data is no longer reliable. + resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(), + RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE); } mEnergyConsumerStatsConfig = config; @@ -15456,18 +15457,14 @@ public class BatteryStatsImpl extends BatteryStats { mWifiPowerCalculator = new WifiPowerCalculator(mPowerProfile); } } else { - compatibleConfig = (mEnergyConsumerStatsConfig == null); - // EnergyConsumer no longer supported, wipe out the existing data. + if (mEnergyConsumerStatsConfig != null) { + // EnergyConsumer no longer supported, wipe out the existing data. + resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(), + RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE); + } mEnergyConsumerStatsConfig = null; mGlobalEnergyConsumerStats = null; } - - if (!compatibleConfig) { - // Supported power buckets changed since last boot. - // Existing data is no longer reliable. - resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(), - RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE); - } } @GuardedBy("this") diff --git a/services/core/java/com/android/server/search/SearchManagerService.java b/services/core/java/com/android/server/search/SearchManagerService.java index 7091c47b8e83..ecfc040ae29c 100644 --- a/services/core/java/com/android/server/search/SearchManagerService.java +++ b/services/core/java/com/android/server/search/SearchManagerService.java @@ -17,6 +17,7 @@ package com.android.server.search; import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.app.ISearchManager; import android.app.SearchManager; import android.app.SearchableInfo; @@ -24,6 +25,7 @@ import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.ContentObserver; import android.os.Binder; @@ -32,6 +34,7 @@ import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; @@ -47,6 +50,7 @@ import com.android.server.statusbar.StatusBarManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.List; /** @@ -71,11 +75,6 @@ public class SearchManagerService extends ISearchManager.Stub { } @Override - public void onUserUnlocking(@NonNull TargetUser user) { - mService.mHandler.post(() -> mService.onUnlockUser(user.getUserIdentifier())); - } - - @Override public void onUserStopped(@NonNull TargetUser user) { mService.onCleanupUser(user.getUserIdentifier()); } @@ -102,10 +101,6 @@ public class SearchManagerService extends ISearchManager.Stub { } private Searchables getSearchables(int userId) { - return getSearchables(userId, false); - } - - private Searchables getSearchables(int userId, boolean forceUpdate) { final long token = Binder.clearCallingIdentity(); try { final UserManager um = mContext.getSystemService(UserManager.class); @@ -122,21 +117,11 @@ public class SearchManagerService extends ISearchManager.Stub { Searchables searchables = mSearchables.get(userId); if (searchables == null) { searchables = new Searchables(mContext, userId); - searchables.updateSearchableList(); - mSearchables.append(userId, searchables); - } else if (forceUpdate) { - searchables.updateSearchableList(); + mSearchables.put(userId, searchables); } - return searchables; - } - } - private void onUnlockUser(int userId) { - try { - getSearchables(userId, true); - } catch (IllegalStateException ignored) { - // We're just trying to warm a cache, so we don't mind if the user - // was stopped or destroyed before we got here. + searchables.updateSearchableListIfNeeded(); + return searchables; } } @@ -150,28 +135,110 @@ public class SearchManagerService extends ISearchManager.Stub { * Refreshes the "searchables" list when packages are added/removed. */ class MyPackageMonitor extends PackageMonitor { + /** + * Packages that are appeared, disappeared, or modified for whatever reason. + */ + private final ArrayList<String> mChangedPackages = new ArrayList<>(); + + /** + * {@code true} if one or more packages that contain {@link SearchableInfo} appeared. + */ + private boolean mSearchablePackageAppeared = false; + + @Override + public void onBeginPackageChanges() { + clearPackageChangeState(); + } + + @Override + public void onPackageAppeared(String packageName, int reason) { + if (!mSearchablePackageAppeared) { + // Check if the new appeared package contains SearchableInfo. + mSearchablePackageAppeared = + hasSearchableForPackage(packageName, getChangingUserId()); + } + mChangedPackages.add(packageName); + } @Override - public void onSomePackagesChanged() { - updateSearchables(); + public void onPackageDisappeared(String packageName, int reason) { + mChangedPackages.add(packageName); } @Override - public void onPackageModified(String pkg) { - updateSearchables(); + public void onPackageModified(String packageName) { + mChangedPackages.add(packageName); } - private void updateSearchables() { + @Override + public void onFinishPackageChanges() { + onFinishPackageChangesInternal(); + clearPackageChangeState(); + } + + private void clearPackageChangeState() { + mChangedPackages.clear(); + mSearchablePackageAppeared = false; + } + + private boolean hasSearchableForPackage(String packageName, int userId) { + final List<ResolveInfo> searchList = querySearchableActivities(mContext, + new Intent(Intent.ACTION_SEARCH).setPackage(packageName), userId); + if (!searchList.isEmpty()) { + return true; + } + + final List<ResolveInfo> webSearchList = querySearchableActivities(mContext, + new Intent(Intent.ACTION_WEB_SEARCH).setPackage(packageName), userId); + if (!webSearchList.isEmpty()) { + return true; + } + + final List<ResolveInfo> globalSearchList = querySearchableActivities(mContext, + new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH).setPackage(packageName), + userId); + return !globalSearchList.isEmpty(); + } + + private boolean shouldRebuildSearchableList(@UserIdInt int changingUserId) { + // This method is guaranteed to be called only on getRegisteredHandler() + if (mSearchablePackageAppeared) { + return true; + } + + ArraySet<String> knownSearchablePackageNames = new ArraySet<>(); + synchronized (mSearchables) { + Searchables searchables = mSearchables.get(changingUserId); + if (searchables != null) { + knownSearchablePackageNames = searchables.getKnownSearchablePackageNames(); + } + } + + final int numOfPackages = mChangedPackages.size(); + for (int i = 0; i < numOfPackages; i++) { + final String packageName = mChangedPackages.get(i); + if (knownSearchablePackageNames.contains(packageName)) { + return true; + } + } + + return false; + } + + private void onFinishPackageChangesInternal() { final int changingUserId = getChangingUserId(); + if (!shouldRebuildSearchableList(changingUserId)) { + return; + } + synchronized (mSearchables) { - // Update list of searchable activities - for (int i = 0; i < mSearchables.size(); i++) { - if (changingUserId == mSearchables.keyAt(i)) { - mSearchables.valueAt(i).updateSearchableList(); - break; - } + // Invalidate the searchable list. + Searchables searchables = mSearchables.get(changingUserId); + if (searchables != null) { + searchables.invalidateSearchableList(); } } + // Inform all listeners that the list of searchables has been updated. Intent intent = new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING @@ -180,6 +247,17 @@ public class SearchManagerService extends ISearchManager.Stub { } } + @NonNull + static List<ResolveInfo> querySearchableActivities(Context context, Intent searchIntent, + @UserIdInt int userId) { + final List<ResolveInfo> activities = context.getPackageManager() + .queryIntentActivitiesAsUser(searchIntent, PackageManager.GET_META_DATA + | PackageManager.MATCH_INSTANT + | PackageManager.MATCH_DEBUG_TRIAGED_MISSING, userId); + return activities; + } + + class GlobalSearchProviderObserver extends ContentObserver { private final ContentResolver mResolver; @@ -196,7 +274,7 @@ public class SearchManagerService extends ISearchManager.Stub { public void onChange(boolean selfChange) { synchronized (mSearchables) { for (int i = 0; i < mSearchables.size(); i++) { - mSearchables.valueAt(i).updateSearchableList(); + mSearchables.valueAt(i).invalidateSearchableList(); } } Intent intent = new Intent(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); diff --git a/services/core/java/com/android/server/search/Searchables.java b/services/core/java/com/android/server/search/Searchables.java index 7b397755173d..dc6733941357 100644 --- a/services/core/java/com/android/server/search/Searchables.java +++ b/services/core/java/com/android/server/search/Searchables.java @@ -35,8 +35,10 @@ import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; +import android.util.ArraySet; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.server.LocalServices; import java.io.FileDescriptor; @@ -62,7 +64,6 @@ public class Searchables { private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*"; private Context mContext; - private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null; private ArrayList<SearchableInfo> mSearchablesList = null; private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null; @@ -81,6 +82,12 @@ public class Searchables { final private IPackageManager mPm; // User for which this Searchables caches information private int mUserId; + @GuardedBy("this") + private boolean mRebuildSearchables = true; + + // Package names that are known to contain {@link SearchableInfo} + @GuardedBy("this") + private ArraySet<String> mKnownSearchablePackageNames = new ArraySet<>(); /** * @@ -224,7 +231,14 @@ public class Searchables { * * TODO: sort the list somehow? UI choice. */ - public void updateSearchableList() { + public void updateSearchableListIfNeeded() { + synchronized (this) { + if (!mRebuildSearchables) { + // The searchable list is valid, no need to rebuild. + return; + } + } + // These will become the new values at the end of the method HashMap<ComponentName, SearchableInfo> newSearchablesMap = new HashMap<ComponentName, SearchableInfo>(); @@ -232,6 +246,7 @@ public class Searchables { = new ArrayList<SearchableInfo>(); ArrayList<SearchableInfo> newSearchablesInGlobalSearchList = new ArrayList<SearchableInfo>(); + ArraySet<String> newKnownSearchablePackageNames = new ArraySet<>(); // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers. List<ResolveInfo> searchList; @@ -264,6 +279,7 @@ public class Searchables { mUserId); if (searchable != null) { newSearchablesList.add(searchable); + newKnownSearchablePackageNames.add(ai.packageName); newSearchablesMap.put(searchable.getSearchActivity(), searchable); if (searchable.shouldIncludeInGlobalSearch()) { newSearchablesInGlobalSearchList.add(searchable); @@ -286,16 +302,41 @@ public class Searchables { synchronized (this) { mSearchablesMap = newSearchablesMap; mSearchablesList = newSearchablesList; + mKnownSearchablePackageNames = newKnownSearchablePackageNames; mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList; mGlobalSearchActivities = newGlobalSearchActivities; mCurrentGlobalSearchActivity = newGlobalSearchActivity; mWebSearchActivity = newWebSearchActivity; + for (ResolveInfo globalSearchActivity: mGlobalSearchActivities) { + mKnownSearchablePackageNames.add( + globalSearchActivity.getComponentInfo().packageName); + } + if (mCurrentGlobalSearchActivity != null) { + mKnownSearchablePackageNames.add( + mCurrentGlobalSearchActivity.getPackageName()); + } + if (mWebSearchActivity != null) { + mKnownSearchablePackageNames.add(mWebSearchActivity.getPackageName()); + } + + mRebuildSearchables = false; } } finally { Binder.restoreCallingIdentity(ident); } } + synchronized ArraySet<String> getKnownSearchablePackageNames() { + return mKnownSearchablePackageNames; + } + + synchronized void invalidateSearchableList() { + mRebuildSearchables = true; + + // Don't rebuild the searchable list, it will be rebuilt + // when the next updateSearchableList gets called. + } + /** * Returns a sorted list of installed search providers as per * the following heuristics: @@ -532,6 +573,8 @@ public class Searchables { pw.print(" "); pw.println(info.getSuggestAuthority()); } } + + pw.println("mRebuildSearchables = " + mRebuildSearchables); } } } diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java index 7163319f281a..5d17884c769b 100644 --- a/services/core/java/com/android/server/vibrator/VibrationScaler.java +++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java @@ -19,7 +19,7 @@ package com.android.server.vibrator; import android.annotation.NonNull; import android.content.Context; import android.hardware.vibrator.V1_0.EffectStrength; -import android.os.IExternalVibratorService; +import android.os.ExternalVibrationScale; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; @@ -37,11 +37,13 @@ final class VibrationScaler { // Scale levels. Each level, except MUTE, is defined as the delta between the current setting // and the default intensity for that type of vibration (i.e. current - default). - private static final int SCALE_VERY_LOW = IExternalVibratorService.SCALE_VERY_LOW; // -2 - private static final int SCALE_LOW = IExternalVibratorService.SCALE_LOW; // -1 - private static final int SCALE_NONE = IExternalVibratorService.SCALE_NONE; // 0 - private static final int SCALE_HIGH = IExternalVibratorService.SCALE_HIGH; // 1 - private static final int SCALE_VERY_HIGH = IExternalVibratorService.SCALE_VERY_HIGH; // 2 + private static final int SCALE_VERY_LOW = + ExternalVibrationScale.ScaleLevel.SCALE_VERY_LOW; // -2 + private static final int SCALE_LOW = ExternalVibrationScale.ScaleLevel.SCALE_LOW; // -1 + private static final int SCALE_NONE = ExternalVibrationScale.ScaleLevel.SCALE_NONE; // 0 + private static final int SCALE_HIGH = ExternalVibrationScale.ScaleLevel.SCALE_HIGH; // 1 + private static final int SCALE_VERY_HIGH = + ExternalVibrationScale.ScaleLevel.SCALE_VERY_HIGH; // 2 // Scale factors for each level. private static final float SCALE_FACTOR_VERY_LOW = 0.6f; @@ -83,9 +85,9 @@ final class VibrationScaler { * Calculates the scale to be applied to external vibration with given usage. * * @param usageHint one of VibrationAttributes.USAGE_* - * @return one of IExternalVibratorService.SCALE_* + * @return one of ExternalVibrationScale.ScaleLevel.SCALE_* */ - public int getExternalVibrationScale(int usageHint) { + public int getExternalVibrationScaleLevel(int usageHint) { int defaultIntensity = mSettingsController.getDefaultIntensity(usageHint); int currentIntensity = mSettingsController.getCurrentIntensity(usageHint); @@ -107,6 +109,22 @@ final class VibrationScaler { } /** + * Returns the adaptive haptics scale that should be applied to the vibrations with + * the given usage. When no adaptive scales are available for the usages, then returns 1 + * indicating no scaling will be applied + * + * @param usageHint one of VibrationAttributes.USAGE_* + * @return The adaptive haptics scale. + */ + public float getAdaptiveHapticsScale(int usageHint) { + if (shouldApplyAdaptiveHapticsScale(usageHint)) { + return mAdaptiveHapticsScales.get(usageHint); + } + + return 1f; // no scaling + } + + /** * Scale a {@link VibrationEffect} based on the given usage hint for this vibration. * * @param effect the effect to be scaled @@ -152,9 +170,7 @@ final class VibrationScaler { } // If adaptive haptics scaling is available for this usage, apply it to the segment. - if (Flags.adaptiveHapticsEnabled() - && mAdaptiveHapticsScales.size() > 0 - && mAdaptiveHapticsScales.contains(usageHint)) { + if (shouldApplyAdaptiveHapticsScale(usageHint)) { float adaptiveScale = mAdaptiveHapticsScales.get(usageHint); segment = segment.scaleLinearly(adaptiveScale); } @@ -224,6 +240,10 @@ final class VibrationScaler { mAdaptiveHapticsScales.clear(); } + private boolean shouldApplyAdaptiveHapticsScale(int usageHint) { + return Flags.adaptiveHapticsEnabled() && mAdaptiveHapticsScales.contains(usageHint); + } + /** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */ private static int intensityToEffectStrength(int intensity) { switch (intensity) { diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index be5d15877e32..78e0ebbb53fa 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import static android.os.ExternalVibrationScale.ScaleLevel.SCALE_MUTE; import static android.os.VibrationEffect.VibrationParameter.targetAmplitude; import static android.os.VibrationEffect.VibrationParameter.targetFrequency; @@ -35,6 +36,7 @@ import android.os.Binder; import android.os.Build; import android.os.CombinedVibration; import android.os.ExternalVibration; +import android.os.ExternalVibrationScale; import android.os.Handler; import android.os.IBinder; import android.os.IExternalVibratorService; @@ -277,7 +279,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { context.registerReceiver(mIntentReceiver, filter, Context.RECEIVER_NOT_EXPORTED); injector.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService()); - if (ServiceManager.isDeclared(VIBRATOR_CONTROL_SERVICE)) { + if (injector.isServiceDeclared(VIBRATOR_CONTROL_SERVICE)) { injector.addService(VIBRATOR_CONTROL_SERVICE, mVibratorControlService); } @@ -1427,6 +1429,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { VibratorControllerHolder createVibratorControllerHolder() { return new VibratorControllerHolder(); } + + boolean isServiceDeclared(String name) { + return ServiceManager.isDeclared(name); + } } /** @@ -1594,7 +1600,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { IBinder.DeathRecipient { public final ExternalVibration externalVibration; - public int scale; + public ExternalVibrationScale scale = new ExternalVibrationScale(); private Vibration.Status mStatus; @@ -1605,7 +1611,6 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // instead of using DEVICE_ID_INVALID here and relying on the UID checks. Context.DEVICE_ID_INVALID, externalVibration.getPackage(), null)); this.externalVibration = externalVibration; - this.scale = IExternalVibratorService.SCALE_NONE; mStatus = Vibration.Status.RUNNING; } @@ -1658,7 +1663,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { public Vibration.DebugInfo getDebugInfo() { return new Vibration.DebugInfo(mStatus, stats, /* playedEffect= */ null, - /* originalEffect= */ null, scale, callerInfo); + /* originalEffect= */ null, scale.scaleLevel, callerInfo); } public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) { @@ -1988,11 +1993,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { /** Implementation of {@link IExternalVibratorService} to be triggered on external control. */ @VisibleForTesting final class ExternalVibratorService extends IExternalVibratorService.Stub { + private static final ExternalVibrationScale SCALE_MUTE = new ExternalVibrationScale(); + + static { + SCALE_MUTE.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE; + } @Override - public int onExternalVibrationStart(ExternalVibration vib) { + public ExternalVibrationScale onExternalVibrationStart(ExternalVibration vib) { + if (!hasExternalControlCapability()) { - return IExternalVibratorService.SCALE_MUTE; + return SCALE_MUTE; } if (ActivityManager.checkComponentPermission(android.Manifest.permission.VIBRATE, vib.getUid(), -1 /*owningUid*/, true /*exported*/) @@ -2000,7 +2011,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid() + " tried to play externally controlled vibration" + " without VIBRATE permission, ignoring."); - return IExternalVibratorService.SCALE_MUTE; + return SCALE_MUTE; } // Create Vibration.Stats as close to the received request as possible, for tracking. @@ -2033,7 +2044,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } if (vibrationEndInfo != null) { - vibHolder.scale = IExternalVibratorService.SCALE_MUTE; + vibHolder.scale = SCALE_MUTE; // Failed to start the vibration, end it and report metrics right away. endVibrationAndWriteStatsLocked(vibHolder, vibrationEndInfo); return vibHolder.scale; @@ -2074,7 +2085,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } mCurrentExternalVibration = vibHolder; vibHolder.linkToDeath(); - vibHolder.scale = mVibrationScaler.getExternalVibrationScale(attrs.getUsage()); + vibHolder.scale.scaleLevel = mVibrationScaler.getExternalVibrationScaleLevel( + attrs.getUsage()); + vibHolder.scale.adaptiveHapticsScale = mVibrationScaler.getAdaptiveHapticsScale( + attrs.getUsage()); } if (waitForCompletion) { @@ -2086,7 +2100,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_CANCELLING), /* continueExternalControl= */ false); } - return IExternalVibratorService.SCALE_MUTE; + return SCALE_MUTE; } } if (!alreadyUnderExternalControl) { diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index b3c8b0b3a47a..27c80c43cd94 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -107,15 +107,20 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { mContext = context; mSystemInterface = systemInterface; WebViewProviderInfo[] webviewProviders = getWebViewPackages(); + + WebViewProviderInfo defaultProvider = null; for (WebViewProviderInfo provider : webviewProviders) { if (provider.availableByDefault) { - mDefaultProvider = provider; + defaultProvider = provider; break; } } - // This should be unreachable because the config parser enforces that there is at least one - // availableByDefault provider. - throw new AndroidRuntimeException("No available by default WebView Provider."); + if (defaultProvider == null) { + // This should be unreachable because the config parser enforces that there is at least + // one availableByDefault provider. + throw new AndroidRuntimeException("No available by default WebView Provider."); + } + mDefaultProvider = defaultProvider; } @Override diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 56024f75545c..bc6f93fd64ac 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1535,11 +1535,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Picture-in-picture mode changes also trigger a multi-window mode change as well, so // update that here in order. Set the last reported MW state to the same as the PiP // state since we haven't yet actually resized the task (these callbacks need to - // precede the configuration change from the resize. + // precede the configuration change from the resize.) mLastReportedPictureInPictureMode = inPictureInPictureMode; mLastReportedMultiWindowMode = inPictureInPictureMode; ensureActivityConfiguration(true /* ignoreVisibility */); - if (inPictureInPictureMode && findMainWindow() == null) { + if (inPictureInPictureMode && findMainWindow() == null + && task.topRunningActivity() == this) { // Prevent malicious app entering PiP without valid WindowState, which can in turn // result a non-touchable PiP window since the InputConsumer for PiP requires it. EventLog.writeEvent(0x534e4554, "265293293", -1, ""); @@ -3549,7 +3550,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A IBinder callerToken = new Binder(); if (android.security.Flags.contentUriPermissionApis()) { try { - resultTo.computeCallerInfo(callerToken, intent, this.getUid(), + resultTo.computeCallerInfo(callerToken, resultData, this.getUid(), mAtmService.getPackageManager().getNameForUid(this.getUid()), /* isShareIdentityEnabled */ false); // Result callers cannot share their identity via diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 10cbc6633533..85d81c4db2ca 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -104,6 +104,7 @@ import android.window.TaskFragmentOrganizerToken; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.ToBooleanFunction; import com.android.server.am.HostingRecord; import com.android.server.pm.pkg.AndroidPackage; import com.android.window.flags.Flags; @@ -3025,11 +3026,17 @@ class TaskFragment extends WindowContainer<WindowContainer> { return false; } - // boost if there's an Activity window that has FLAG_DIM_BEHIND flag. - return forAllWindows( + ToBooleanFunction<WindowState> getDimBehindWindow = (w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null && w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested() - || w.mActivityRecord.isVisible()), true); + || w.mActivityRecord.isVisible()); + if (adjacentTf.forAllWindows(getDimBehindWindow, true)) { + // early return if the adjacent Tf has a dimming window. + return false; + } + + // boost if there's an Activity window that has FLAG_DIM_BEHIND flag. + return forAllWindows(getDimBehindWindow, true); } @Override diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index a46916553abc..b38a2f9558e9 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -117,6 +117,9 @@ <xs:element type="sensorDetails" name="proxSensor"> <xs:annotation name="final"/> </xs:element> + <xs:element type="sensorDetails" name="tempSensor"> + <xs:annotation name="final"/> + </xs:element> <!-- Length of the ambient light horizon used to calculate the long & short term estimates of ambient light in milliseconds.--> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index 79ea274e2fca..b329db4a2076 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -133,6 +133,7 @@ package com.android.server.display.config { method public final java.math.BigDecimal getScreenBrightnessRampSlowIncreaseIdle(); method public final com.android.server.display.config.SensorDetails getScreenOffBrightnessSensor(); method public final com.android.server.display.config.IntegerArray getScreenOffBrightnessSensorValueToLux(); + method public final com.android.server.display.config.SensorDetails getTempSensor(); method @NonNull public final com.android.server.display.config.ThermalThrottling getThermalThrottling(); method public final com.android.server.display.config.UsiVersion getUsiVersion(); method public final void setAmbientBrightnessChangeThresholds(@NonNull com.android.server.display.config.Thresholds); @@ -167,6 +168,7 @@ package com.android.server.display.config { method public final void setScreenBrightnessRampSlowIncreaseIdle(java.math.BigDecimal); method public final void setScreenOffBrightnessSensor(com.android.server.display.config.SensorDetails); method public final void setScreenOffBrightnessSensorValueToLux(com.android.server.display.config.IntegerArray); + method public final void setTempSensor(com.android.server.display.config.SensorDetails); method public final void setThermalThrottling(@NonNull com.android.server.display.config.ThermalThrottling); method public final void setUsiVersion(com.android.server.display.config.UsiVersion); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index ca23d62601bb..fa63bc899cb5 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -30,9 +30,7 @@ import android.credentials.selection.AuthenticationEntry; import android.credentials.selection.Entry; import android.credentials.selection.GetCredentialProviderData; import android.credentials.selection.ProviderPendingIntentResponse; -import android.os.Bundle; import android.os.ICancellationSignal; -import android.service.autofill.Flags; import android.service.credentials.Action; import android.service.credentials.BeginGetCredentialOption; import android.service.credentials.BeginGetCredentialRequest; @@ -44,7 +42,6 @@ import android.service.credentials.GetCredentialRequest; import android.service.credentials.RemoteEntry; import android.util.Pair; import android.util.Slog; -import android.view.autofill.AutofillId; import java.util.ArrayList; import java.util.HashMap; @@ -77,10 +74,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential @NonNull private final Map<String, CredentialOption> mBeginGetOptionToCredentialOptionMap; - @NonNull - private final Map<String, AutofillId> mCredentialEntryKeyToAutofilLIdMap; - - /** The complete request to be used in the second round. */ private final android.credentials.GetCredentialRequest mCompleteRequest; private final CallingAppInfo mCallingAppInfo; @@ -249,7 +242,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential mBeginGetOptionToCredentialOptionMap = new HashMap<>(beginGetOptionToCredentialOptionMap); mProviderResponseDataHandler = new ProviderResponseDataHandler( ComponentName.unflattenFromString(hybridService)); - mCredentialEntryKeyToAutofilLIdMap = new HashMap<>(); } /** Called when the provider response has been updated by an external source. */ @@ -303,7 +295,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential invokeCallbackOnInternalInvalidState(); return; } - onCredentialEntrySelected(providerPendingIntentResponse, entryKey); + onCredentialEntrySelected(providerPendingIntentResponse); break; case ACTION_ENTRY_KEY: Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey); @@ -312,7 +304,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential invokeCallbackOnInternalInvalidState(); return; } - onActionEntrySelected(providerPendingIntentResponse, entryKey); + onActionEntrySelected(providerPendingIntentResponse); break; case AUTHENTICATION_ACTION_ENTRY_KEY: Action authenticationEntry = mProviderResponseDataHandler @@ -342,7 +334,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential break; case REMOTE_ENTRY_KEY: if (mProviderResponseDataHandler.getRemoteEntry(entryKey) != null) { - onRemoteEntrySelected(providerPendingIntentResponse, entryKey); + onRemoteEntrySelected(providerPendingIntentResponse); } else { Slog.i(TAG, "Unexpected remote entry key"); invokeCallbackOnInternalInvalidState(); @@ -381,7 +373,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential return null; } - private Intent setUpFillInIntentWithFinalRequest(@NonNull String id, String entryKey) { + 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 @@ -392,13 +384,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential Slog.w(TAG, "Id from Credential Entry does not resolve to a valid option"); return intent; } - AutofillId autofillId = credentialOption - .getCandidateQueryData() - .getParcelable(CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class); - if (autofillId != null && Flags.autofillCredmanIntegration()) { - intent.putExtra(CredentialProviderService.EXTRA_AUTOFILL_ID, autofillId); - mCredentialEntryKeyToAutofilLIdMap.put(entryKey, autofillId); - } return intent.putExtra( CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST, new GetCredentialRequest( @@ -414,13 +399,12 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential } private void onRemoteEntrySelected( - ProviderPendingIntentResponse providerPendingIntentResponse, String entryKey) { - onCredentialEntrySelected(providerPendingIntentResponse, entryKey); + ProviderPendingIntentResponse providerPendingIntentResponse) { + onCredentialEntrySelected(providerPendingIntentResponse); } private void onCredentialEntrySelected( - ProviderPendingIntentResponse providerPendingIntentResponse, - String entryKey) { + ProviderPendingIntentResponse providerPendingIntentResponse) { if (providerPendingIntentResponse == null) { invokeCallbackOnInternalInvalidState(); return; @@ -437,18 +421,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential GetCredentialResponse getCredentialResponse = PendingIntentResultHandler .extractGetCredentialResponse( providerPendingIntentResponse.getResultData()); - if (getCredentialResponse != null && getCredentialResponse.getCredential() != null) { - Bundle credentialData = getCredentialResponse.getCredential().getData(); - AutofillId autofillId = mCredentialEntryKeyToAutofilLIdMap.get(entryKey); - if (Flags.autofillCredmanIntegration() - && entryKey != null && autofillId != null && credentialData != null - ) { - Slog.d(TAG, "Adding autofillId to credential response: " + autofillId); - credentialData.putParcelable( - CredentialProviderService.EXTRA_AUTOFILL_ID, - mCredentialEntryKeyToAutofilLIdMap.get(entryKey) - ); - } + if (getCredentialResponse != null) { mCallbacks.onFinalResponseReceived(mComponentName, getCredentialResponse); return; @@ -532,9 +505,9 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential /** Returns true if either an exception or a response is found. */ private void onActionEntrySelected(ProviderPendingIntentResponse - providerPendingIntentResponse, String entryKey) { + providerPendingIntentResponse) { Slog.i(TAG, "onActionEntrySelected"); - onCredentialEntrySelected(providerPendingIntentResponse, entryKey); + onCredentialEntrySelected(providerPendingIntentResponse); } @@ -632,7 +605,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential Entry entry = new Entry(CREDENTIAL_ENTRY_KEY, id, credentialEntry.getSlice(), setUpFillInIntentWithFinalRequest(credentialEntry - .getBeginGetCredentialOptionId(), id)); + .getBeginGetCredentialOptionId())); mUiCredentialEntries.put(id, new Pair<>(credentialEntry, entry)); mCredentialEntryTypes.add(credentialEntry.getType()); } diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp index afd6dbd7f6a7..3bce9b54e320 100644 --- a/services/tests/InputMethodSystemServerTests/Android.bp +++ b/services/tests/InputMethodSystemServerTests/Android.bp @@ -69,7 +69,7 @@ android_test { } android_ravenwood_test { - name: "FrameworksInputMethodSystemServerTests_host", + name: "FrameworksInputMethodSystemServerTestsRavenwood", static_libs: [ "androidx.annotation_annotation", "androidx.test.rules", @@ -85,7 +85,6 @@ android_ravenwood_test { srcs: [ "src/com/android/server/inputmethod/**/ClientControllerTest.java", ], - sdk_version: "test_current", auto_gen_config: true, } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java index dc9631a8f2e2..9e3d9ec6b9b6 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java @@ -32,10 +32,8 @@ import android.content.pm.PackageManagerInternal; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.ravenwood.RavenwoodRule; import android.view.Display; -import android.view.inputmethod.InputBinding; import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IRemoteInputConnection; @@ -53,7 +51,7 @@ import java.util.concurrent.TimeUnit; public final class ClientControllerTest { private static final int ANY_DISPLAY_ID = Display.DEFAULT_DISPLAY; private static final int ANY_CALLER_UID = 1; - private static final int ANY_CALLER_PID = 1; + private static final int ANY_CALLER_PID = 2; private static final String SOME_PACKAGE_NAME = "some.package"; @Rule @@ -82,13 +80,16 @@ public final class ClientControllerTest { mController = new ClientController(mMockPackageManagerInternal); } + // TODO(b/322895594): No need to directly invoke create$ravenwood once b/322895594 is fixed. + private IInputMethodClientInvoker createInvoker(IInputMethodClient client, Handler handler) { + return RavenwoodRule.isOnRavenwood() + ? IInputMethodClientInvoker.create$ravenwood(client, handler) : + IInputMethodClientInvoker.create(client, handler); + } + @Test - // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for - // inputmethod server classes. - @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testAddClient_cannotAddTheSameClientTwice() { - var invoker = IInputMethodClientInvoker.create(mClient, mHandler); - + final var invoker = createInvoker(mClient, mHandler); synchronized (ImfLock.class) { mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, ANY_CALLER_PID); @@ -101,18 +102,17 @@ public final class ClientControllerTest { } }); assertThat(thrown.getMessage()).isEqualTo( - "uid=1/pid=1/displayId=0 is already registered"); + "uid=" + ANY_CALLER_UID + "/pid=" + ANY_CALLER_PID + + "/displayId=0 is already registered"); } } @Test - // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for - // inputmethod server classes. - @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testAddClient() throws Exception { + final var invoker = createInvoker(mClient, mHandler); synchronized (ImfLock.class) { - var invoker = IInputMethodClientInvoker.create(mClient, mHandler); - var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, + final var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, + ANY_CALLER_UID, ANY_CALLER_PID); verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0)); @@ -121,16 +121,12 @@ public final class ClientControllerTest { } @Test - // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for - // inputmethod server classes. - @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testRemoveClient() { - var callback = new TestClientControllerCallback(); + final var invoker = createInvoker(mClient, mHandler); + final var callback = new TestClientControllerCallback(); ClientState added; synchronized (ImfLock.class) { mController.addClientControllerCallback(callback); - - var invoker = IInputMethodClientInvoker.create(mClient, mHandler); added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, ANY_CALLER_PID); assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added); @@ -138,21 +134,17 @@ public final class ClientControllerTest { } // Test callback - var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS); + final var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS); assertThat(removed).isSameInstanceAs(added); } @Test - // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for - // inputmethod server classes and updated to newer Mockito with static mock support (mock - // InputMethodUtils#checkIfPackageBelongsToUid instead of PackageManagerInternal#isSameApp) - @IgnoreUnderRavenwood(blockedBy = {InputMethodUtils.class}) public void testVerifyClientAndPackageMatch() { + final var invoker = createInvoker(mClient, mHandler); when(mMockPackageManagerInternal.isSameApp(eq(SOME_PACKAGE_NAME), /* flags= */ anyLong(), eq(ANY_CALLER_UID), /* userId= */ anyInt())).thenReturn(true); synchronized (ImfLock.class) { - var invoker = IInputMethodClientInvoker.create(mClient, mHandler); mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, ANY_CALLER_PID); assertThat( diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java index 8faaf5998d13..05c243fda2b8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java @@ -43,6 +43,7 @@ import com.android.internal.os.BackgroundThread; import com.android.server.display.BrightnessThrottler.Injector; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; +import com.android.server.display.config.SensorData; import com.android.server.display.mode.DisplayModeDirectorTest; import org.junit.Before; @@ -56,6 +57,7 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; @SmallTest @RunWith(AndroidJUnit4.class) @@ -292,6 +294,53 @@ public class BrightnessThrottlerTest { assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason()); } + + @Test + public void testThermalThrottlingWithDisplaySensor() throws Exception { + final ThrottlingLevel level = + new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f); + List<ThrottlingLevel> levels = new ArrayList<>(List.of(level)); + final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels); + final SensorData tempSensor = new SensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY"); + final BrightnessThrottler throttler = + createThrottlerSupportedWithTempSensor(data, tempSensor); + assertTrue(throttler.deviceSupportsThrottling()); + + verify(mThermalServiceMock) + .registerThermalEventListenerWithType( + mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_DISPLAY)); + final IThermalEventListener listener = mThermalEventListenerCaptor.getValue(); + + // Set VIRTUAL-SKIN-DISPLAY tatus too low to verify no throttling. + listener.notifyThrottling(getDisplayTempWithName(tempSensor.name, level.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason()); + + // Verify when skin sensor throttled, no brightness throttling triggered. + listener.notifyThrottling(getSkinTemp(level.thermalStatus + 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason()); + + // Verify when display sensor of another name throttled, no brightness throttling triggered. + listener.notifyThrottling(getDisplayTempWithName("ANOTHER-NAME", level.thermalStatus + 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason()); + + // Verify when display sensor of current name throttled, brightness throttling triggered. + listener.notifyThrottling(getDisplayTempWithName(tempSensor.name, level.thermalStatus + 1)); + mTestLooper.dispatchAll(); + assertEquals(level.brightness, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL, + throttler.getBrightnessMaxReason()); + } + @Test public void testUpdateThermalThrottlingData() throws Exception { // Initialise brightness throttling levels // Ensure that they are overridden by setting the data through device config. @@ -476,18 +525,30 @@ public class BrightnessThrottlerTest { return new BrightnessThrottler(mInjectorMock, mHandler, mHandler, /* throttlingChangeCallback= */ () -> {}, /* uniqueDisplayId= */ null, /* thermalThrottlingDataId= */ null, - /* thermalThrottlingDataMap= */ new HashMap<>(1)); + /* thermalThrottlingDataMap= */ new HashMap<>(1), + /* tempSensor= */ null); } private BrightnessThrottler createThrottlerSupported(ThermalBrightnessThrottlingData data) { + SensorData tempSensor = SensorData.loadTempSensorUnspecifiedConfig(); + return createThrottlerSupportedWithTempSensor(data, tempSensor); + } + private BrightnessThrottler createThrottlerSupportedWithTempSensor( + ThermalBrightnessThrottlingData data, SensorData tempSensor) { assertNotNull(data); - HashMap<String, ThermalBrightnessThrottlingData> throttlingDataMap = new HashMap<>(1); + Map<String, ThermalBrightnessThrottlingData> throttlingDataMap = new HashMap<>(1); throttlingDataMap.put("default", data); return new BrightnessThrottler(mInjectorMock, mHandler, BackgroundThread.getHandler(), - () -> {}, "123", "default", throttlingDataMap); + () -> {}, "123", "default", throttlingDataMap, tempSensor); } private Temperature getSkinTemp(@ThrottlingStatus int status) { return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status); } + + private Temperature getDisplayTempWithName( + String sensorName, @ThrottlingStatus int status) { + assertNotNull(sensorName); + return new Temperature(30.0f, Temperature.TYPE_DISPLAY, sensorName, status); + } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index b29fc8828f58..2867041511b5 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -20,6 +20,7 @@ package com.android.server.display; import static com.android.internal.display.BrightnessSynchronizer.brightnessIntToFloat; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE; +import static com.android.server.display.config.SensorData.TEMPERATURE_TYPE_SKIN; import static com.android.server.display.config.SensorData.SupportedMode; import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat; import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat; @@ -106,6 +107,7 @@ public final class DisplayDeviceConfigTest { MockitoAnnotations.initMocks(this); when(mContext.getResources()).thenReturn(mResources); when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(true); + when(mFlags.isSensorBasedBrightnessThrottlingEnabled()).thenReturn(true); mockDeviceConfigs(); } @@ -143,6 +145,8 @@ public final class DisplayDeviceConfigTest { assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name); assertNull(mDisplayDeviceConfig.getProximitySensor().type); assertNull(mDisplayDeviceConfig.getProximitySensor().name); + assertEquals(TEMPERATURE_TYPE_SKIN, mDisplayDeviceConfig.getTempSensor().type); + assertNull(mDisplayDeviceConfig.getTempSensor().name); assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable()); } @@ -592,6 +596,13 @@ public final class DisplayDeviceConfigTest { } @Test + public void testTempSensorFromDisplayConfig() throws IOException { + setupDisplayDeviceConfigFromDisplayConfigFile(); + assertEquals("DISPLAY", mDisplayDeviceConfig.getTempSensor().type); + assertEquals("VIRTUAL-SKIN-DISPLAY", mDisplayDeviceConfig.getTempSensor().name); + } + + @Test public void testBlockingZoneThresholdsFromDisplayConfig() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(); @@ -1356,6 +1367,10 @@ public final class DisplayDeviceConfigTest { + "<name>Test Binned Brightness Sensor</name>\n" + "</screenOffBrightnessSensor>\n" + proxSensor + + "<tempSensor>\n" + + "<type>DISPLAY</type>\n" + + "<name>VIRTUAL-SKIN-DISPLAY</name>\n" + + "</tempSensor>\n" + "<ambientBrightnessChangeThresholds>\n" + "<brighteningThresholds>\n" + "<minimum>10</minimum>\n" diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java index 37d0f6250aaf..34f352e7bf54 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java @@ -37,10 +37,14 @@ import com.android.internal.annotations.Keep; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; +import com.android.server.display.config.SensorData; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.testutils.FakeDeviceConfigInterface; import com.android.server.testutils.TestHandler; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,9 +54,6 @@ import org.mockito.MockitoAnnotations; import java.util.List; -import junitparams.JUnitParamsRunner; -import junitparams.Parameters; - @RunWith(JUnitParamsRunner.class) public class BrightnessThermalClamperTest { @@ -125,13 +126,13 @@ public class BrightnessThermalClamperTest { public void testNotifyThrottlingAfterOnDisplayChange(List<ThrottlingLevel> throttlingLevels, @Temperature.ThrottlingStatus int throttlingStatus, boolean expectedActive, float expectedBrightness) throws RemoteException { - IThermalEventListener thermalEventListener = captureThermalEventListener(); + IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); mClamper.onDisplayChanged(new TestThermalData(throttlingLevels)); mTestHandler.flush(); assertFalse(mClamper.isActive()); assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); - thermalEventListener.notifyThrottling(createTemperature(throttlingStatus)); + thermalEventListener.notifyThrottling(createSkinTemperature(throttlingStatus)); mTestHandler.flush(); assertEquals(expectedActive, mClamper.isActive()); assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); @@ -139,11 +140,11 @@ public class BrightnessThermalClamperTest { @Test @Parameters(method = "testThrottlingData") - public void testOnDisplayChangeAfterNotifyThrottlng(List<ThrottlingLevel> throttlingLevels, + public void testOnDisplayChangeAfterNotifyThrottling(List<ThrottlingLevel> throttlingLevels, @Temperature.ThrottlingStatus int throttlingStatus, boolean expectedActive, float expectedBrightness) throws RemoteException { - IThermalEventListener thermalEventListener = captureThermalEventListener(); - thermalEventListener.notifyThrottling(createTemperature(throttlingStatus)); + IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); + thermalEventListener.notifyThrottling(createSkinTemperature(throttlingStatus)); mTestHandler.flush(); assertFalse(mClamper.isActive()); assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); @@ -156,8 +157,8 @@ public class BrightnessThermalClamperTest { @Test public void testOverrideData() throws RemoteException { - IThermalEventListener thermalEventListener = captureThermalEventListener(); - thermalEventListener.notifyThrottling(createTemperature(Temperature.THROTTLING_SEVERE)); + IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); + thermalEventListener.notifyThrottling(createSkinTemperature(Temperature.THROTTLING_SEVERE)); mTestHandler.flush(); assertFalse(mClamper.isActive()); assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); @@ -183,15 +184,60 @@ public class BrightnessThermalClamperTest { assertEquals(0.4f, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); } - private IThermalEventListener captureThermalEventListener() throws RemoteException { + @Test + public void testDisplaySensorBasedThrottling() throws RemoteException { + final int severity = PowerManager.THERMAL_STATUS_SEVERE; + IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); + // Update config to listen to display type sensor. + final SensorData tempSensor = new SensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY"); + final TestThermalData thermalData = + new TestThermalData( + DISPLAY_ID, + DisplayDeviceConfig.DEFAULT_ID, + List.of(new ThrottlingLevel(severity, 0.5f)), + tempSensor); + mClamper.onDisplayChanged(thermalData); + mTestHandler.flush(); + verify(mMockThermalService).unregisterThermalEventListener(thermalEventListener); + thermalEventListener = captureThermalEventListener(Temperature.TYPE_DISPLAY); + assertFalse(mClamper.isActive()); + + // Verify no throttling triggered when any other sensor notification received. + thermalEventListener.notifyThrottling(createSkinTemperature(severity)); + mTestHandler.flush(); + assertFalse(mClamper.isActive()); + + thermalEventListener.notifyThrottling(createDisplayTemperature("OTHER-SENSOR", severity)); + mTestHandler.flush(); + assertFalse(mClamper.isActive()); + + assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + + // Verify throttling triggered when display sensor of given name throttled. + thermalEventListener.notifyThrottling(createDisplayTemperature(tempSensor.name, severity)); + mTestHandler.flush(); + assertTrue(mClamper.isActive()); + assertEquals(0.5f, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + } + + private IThermalEventListener captureSkinThermalEventListener() throws RemoteException { + return captureThermalEventListener(Temperature.TYPE_SKIN); + } + + private IThermalEventListener captureThermalEventListener(int type) throws RemoteException { ArgumentCaptor<IThermalEventListener> captor = ArgumentCaptor.forClass( IThermalEventListener.class); verify(mMockThermalService).registerThermalEventListenerWithType(captor.capture(), eq( - Temperature.TYPE_SKIN)); + type)); return captor.getValue(); } - private Temperature createTemperature(@Temperature.ThrottlingStatus int status) { + private Temperature createDisplayTemperature( + @NonNull String sensorName, @Temperature.ThrottlingStatus int status) { + return new Temperature(100, Temperature.TYPE_DISPLAY, sensorName, status); + } + + private Temperature createSkinTemperature(@Temperature.ThrottlingStatus int status) { return new Temperature(100, Temperature.TYPE_SKIN, "test_temperature", status); } @@ -217,19 +263,26 @@ public class BrightnessThermalClamperTest { private final String mUniqueDisplayId; private final String mDataId; private final ThermalBrightnessThrottlingData mData; + private final SensorData mTempSensor; private TestThermalData() { - this(DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID, null); + this(DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID, null, + SensorData.loadTempSensorUnspecifiedConfig()); } private TestThermalData(List<ThrottlingLevel> data) { - this(DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID, data); + this(DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID, data, + SensorData.loadTempSensorUnspecifiedConfig()); } - private TestThermalData(String uniqueDisplayId, String dataId, List<ThrottlingLevel> data) { + + private TestThermalData(String uniqueDisplayId, String dataId, List<ThrottlingLevel> data, + SensorData tempSensor) { mUniqueDisplayId = uniqueDisplayId; mDataId = dataId; mData = ThermalBrightnessThrottlingData.create(data); + mTempSensor = tempSensor; } + @NonNull @Override public String getUniqueDisplayId() { @@ -247,5 +300,11 @@ public class BrightnessThermalClamperTest { public ThermalBrightnessThrottlingData getThermalBrightnessThrottlingData() { return mData; } + + @NonNull + @Override + public SensorData getTempSensor() { + return mTempSensor; + } } } diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index a4761555384e..99752212fcbd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -16,6 +16,8 @@ package com.android.server.alarm; import static android.Manifest.permission.SCHEDULE_EXACT_ALARM; +import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN; +import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN; import static android.app.AlarmManager.ELAPSED_REALTIME; import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; import static android.app.AlarmManager.EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED; @@ -42,6 +44,7 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.NULL_DEFAULT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod; @@ -135,6 +138,7 @@ import android.net.Uri; import android.os.BatteryManager; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.Message; @@ -145,9 +149,11 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.platform.test.flag.util.FlagSetException; import android.provider.DeviceConfig; -import android.provider.Settings; import android.text.format.DateFormat; import android.util.ArraySet; import android.util.Log; @@ -215,6 +221,7 @@ public final class AlarmManagerServiceTest { private AppStateTrackerImpl.Listener mListener; private AlarmManagerService.UninstallReceiver mPackageChangesReceiver; private AlarmManagerService.ChargingReceiver mChargingReceiver; + private ActivityManager.UidFrozenStateChangedCallback mUidFrozenStateCallback; private IAppOpsCallback mIAppOpsCallback; private IAlarmManager mBinder; @Mock @@ -240,6 +247,8 @@ public final class AlarmManagerServiceTest { @Mock private ActivityManagerInternal mActivityManagerInternal; @Mock + private ActivityManager mActivityManager; + @Mock private PackageManagerInternal mPackageManagerInternal; @Mock private AppStateTrackerImpl mAppStateTracker; @@ -403,15 +412,31 @@ public final class AlarmManagerServiceTest { .mockStatic(PermissionChecker.class) .mockStatic(PermissionManagerService.class) .mockStatic(ServiceManager.class) - .mockStatic(Settings.Global.class) .mockStatic(SystemProperties.class) .spyStatic(UserHandle.class) .afterSessionFinished( () -> LocalServices.removeServiceForTest(AlarmManagerInternal.class)) .build(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(NULL_DEFAULT); + + /** + * Have to do this to switch the {@link Flags} implementation to {@link FakeFeatureFlagsImpl}. + * All methods that need any flag enabled should use the + * {@link android.platform.test.annotations.EnableFlags} annotation, in which case disabling + * the flag will fail with an exception that we will swallow here. + */ + private void disableFlagsNotSetByAnnotation() { + try { + mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS); + } catch (FlagSetException fse) { + // Expected if the test about to be run requires this enabled. + } + } + @Before - public final void setUp() { + public void setUp() { doReturn(mIActivityManager).when(ActivityManager::getService); doReturn(mDeviceIdleInternal).when( () -> LocalServices.getService(DeviceIdleInternal.class)); @@ -469,6 +494,7 @@ public final class AlarmManagerServiceTest { when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager); when(mMockContext.getSystemService(BatteryManager.class)).thenReturn(mBatteryManager); + when(mMockContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager); registerAppIds(new String[]{TEST_CALLING_PACKAGE}, new Integer[]{UserHandle.getAppId(TEST_CALLING_UID)}); @@ -479,7 +505,18 @@ public final class AlarmManagerServiceTest { mService = new AlarmManagerService(mMockContext, mInjector); spyOn(mService); + disableFlagsNotSetByAnnotation(); + mService.onStart(); + + if (Flags.useFrozenStateToDropListenerAlarms()) { + final ArgumentCaptor<ActivityManager.UidFrozenStateChangedCallback> frozenCaptor = + ArgumentCaptor.forClass(ActivityManager.UidFrozenStateChangedCallback.class); + verify(mActivityManager).registerUidFrozenStateChangedCallback( + any(HandlerExecutor.class), frozenCaptor.capture()); + mUidFrozenStateCallback = frozenCaptor.getValue(); + } + // Unable to mock mMockContext to return a mock stats manager. // So just mocking the whole MetricsHelper instance. mService.mMetricsHelper = mock(MetricsHelper.class); @@ -3741,9 +3778,87 @@ public final class AlarmManagerServiceTest { mListener.handleUidCachedChanged(TEST_CALLING_UID, true); assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED); assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID)); + assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2)); mListener.handleUidCachedChanged(TEST_CALLING_UID_2, true); assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED); + assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID)); + assertEquals(2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2)); + } + + private void executeUidFrozenStateCallback(int[] uids, int[] frozenStates) { + assertNotNull(mUidFrozenStateCallback); + mUidFrozenStateCallback.onUidFrozenStateChanged(uids, frozenStates); + } + + @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS) + @Test + public void exactListenerAlarmsRemovedOnFrozen() { + mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true); + + setTestAlarmWithListener(ELAPSED_REALTIME, 31, getNewListener(() -> {}), WINDOW_EXACT, + TEST_CALLING_UID); + setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID); + setTestAlarm(ELAPSED_REALTIME, 54, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0, + TEST_CALLING_UID, null); + setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null); + + setTestAlarmWithListener(ELAPSED_REALTIME, 21, getNewListener(() -> {}), WINDOW_EXACT, + TEST_CALLING_UID_2); + setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2); + setTestAlarm(ELAPSED_REALTIME, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0, + TEST_CALLING_UID_2, null); + setTestAlarm(RTC, 549, 234, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID_2, null); + + assertEquals(8, mService.mAlarmStore.size()); + + executeUidFrozenStateCallback( + new int[] {TEST_CALLING_UID, TEST_CALLING_UID_2}, + new int[] {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN}); + assertEquals(7, mService.mAlarmStore.size()); + + executeUidFrozenStateCallback( + new int[] {TEST_CALLING_UID_2}, new int[] {UID_FROZEN_STATE_FROZEN}); + assertEquals(6, mService.mAlarmStore.size()); + } + + @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS) + @Test + public void alarmCountOnListenerFrozen() { + mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true); + + // Set some alarms for TEST_CALLING_UID. + final int numExactListenerUid1 = 17; + for (int i = 0; i < numExactListenerUid1; i++) { + setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i, + getNewListener(() -> {})); + } + setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID); + setTestAlarm(ELAPSED_REALTIME, 54, getNewMockPendingIntent()); + setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null); + + // Set some alarms for TEST_CALLING_UID_2. + final int numExactListenerUid2 = 11; + for (int i = 0; i < numExactListenerUid2; i++) { + setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i, + getNewListener(() -> {}), WINDOW_EXACT, TEST_CALLING_UID_2); + } + setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2); + setTestAlarm(RTC_WAKEUP, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0, + TEST_CALLING_UID_2, null); + + assertEquals(numExactListenerUid1 + 3, mService.mAlarmsPerUid.get(TEST_CALLING_UID)); + assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2)); + + executeUidFrozenStateCallback( + new int[] {TEST_CALLING_UID, TEST_CALLING_UID_2}, + new int[] {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN}); + assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID)); + assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2)); + + executeUidFrozenStateCallback( + new int[] {TEST_CALLING_UID_2}, new int[] {UID_FROZEN_STATE_FROZEN}); + assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID)); assertEquals(2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2)); } diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/restore/PerformUnifiedRestoreTaskTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/restore/PerformUnifiedRestoreTaskTest.java index 940469f89b16..414532b88e22 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/restore/PerformUnifiedRestoreTaskTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/restore/PerformUnifiedRestoreTaskTest.java @@ -29,16 +29,20 @@ import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; import android.app.backup.BackupTransport; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; +import android.os.Build; import android.os.Message; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.modules.utils.testing.TestableDeviceConfig; +import com.android.server.backup.Flags; import com.android.server.backup.UserBackupManagerService; import com.android.server.backup.internal.BackupHandler; import com.android.server.backup.transport.BackupTransportClient; @@ -56,10 +60,12 @@ import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; @@ -74,10 +80,17 @@ public class PerformUnifiedRestoreTaskTest { private static final String SYSTEM_PACKAGE_NAME = "android"; private static final String NON_SYSTEM_PACKAGE_NAME = "package"; - @Mock private BackupDataInput mBackupDataInput; - @Mock private BackupDataOutput mBackupDataOutput; - @Mock private UserBackupManagerService mBackupManagerService; - @Mock private TransportConnection mTransportConnection; + private static final String V_TO_U_ALLOWLIST = "pkg1"; + private static final String V_TO_U_DENYLIST = "pkg2"; + + @Mock + private BackupDataInput mBackupDataInput; + @Mock + private BackupDataOutput mBackupDataOutput; + @Mock + private UserBackupManagerService mBackupManagerService; + @Mock + private TransportConnection mTransportConnection; private Set<String> mExcludedkeys = new HashSet<>(); private Map<String, String> mBackupData = new HashMap<>(); @@ -91,6 +104,10 @@ public class PerformUnifiedRestoreTaskTest { public TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; @Before @@ -118,7 +135,8 @@ public class PerformUnifiedRestoreTaskTest { return null; }); - mRestoreTask = new PerformUnifiedRestoreTask(mBackupManagerService, mTransportConnection); + mRestoreTask = new PerformUnifiedRestoreTask(mBackupManagerService, mTransportConnection, + V_TO_U_ALLOWLIST, V_TO_U_DENYLIST); } private void populateTestData() { @@ -235,6 +253,122 @@ public class PerformUnifiedRestoreTaskTest { == UnifiedRestoreState.FINAL); } + @Test + public void testCreateVToUList_listSettingIsNull_returnEmptyList() { + List<String> expectedEmptyList = new ArrayList<>(); + + List<String> list = mRestoreTask.createVToUList(null); + + assertEquals(list, expectedEmptyList); + } + + @Test + public void testCreateVToUList_listIsNotNull_returnCorrectList() { + List<String> expectedList = Arrays.asList("a", "b", "c"); + String listString = "a,b,c"; + + List<String> list = mRestoreTask.createVToUList(listString); + + assertEquals(list, expectedList); + } + + @Test + public void testIsVToUDowngrade_vToUFlagIsOffAndTargetIsUSourceIsV_returnFalse() { + mSetFlagsRule.disableFlags( + Flags.FLAG_ENABLE_V_TO_U_RESTORE_FOR_SYSTEM_COMPONENTS_IN_ALLOWLIST); + + boolean isVToUDowngrade = mRestoreTask.isVToUDowngrade( + Build.VERSION_CODES.VANILLA_ICE_CREAM, Build.VERSION_CODES.UPSIDE_DOWN_CAKE); + + assertFalse(isVToUDowngrade); + } + + @Test + public void testIsVToUDowngrade_vToUFlagIsOnAndTargetIsUSourceIsV_returnTrue() { + mSetFlagsRule.enableFlags( + Flags.FLAG_ENABLE_V_TO_U_RESTORE_FOR_SYSTEM_COMPONENTS_IN_ALLOWLIST); + + boolean isVToUDowngrade = mRestoreTask.isVToUDowngrade( + Build.VERSION_CODES.VANILLA_ICE_CREAM, Build.VERSION_CODES.UPSIDE_DOWN_CAKE); + + assertTrue(isVToUDowngrade); + } + + @Test + public void testIsVToUDowngrade_vToUFlagIsOnAndSourceIsNotV_returnFalse() { + mSetFlagsRule.enableFlags( + Flags.FLAG_ENABLE_V_TO_U_RESTORE_FOR_SYSTEM_COMPONENTS_IN_ALLOWLIST); + + boolean isVToUDowngrade = mRestoreTask.isVToUDowngrade(Build.VERSION_CODES.UPSIDE_DOWN_CAKE, + Build.VERSION_CODES.UPSIDE_DOWN_CAKE); + + assertFalse(isVToUDowngrade); + } + + @Test + public void testIsVToUDowngrade_vToUFlagIsOnAndTargetIsNotU_returnFalse() { + mSetFlagsRule.enableFlags( + Flags.FLAG_ENABLE_V_TO_U_RESTORE_FOR_SYSTEM_COMPONENTS_IN_ALLOWLIST); + + boolean isVToUDowngrade = mRestoreTask.isVToUDowngrade( + Build.VERSION_CODES.VANILLA_ICE_CREAM, Build.VERSION_CODES.VANILLA_ICE_CREAM); + + assertFalse(isVToUDowngrade); + } + + + @Test + public void testIsEligibleForVToUDowngrade_pkgIsNotOnAllowlist_returnFalse() { + PackageInfo testPackageInfo = new PackageInfo(); + testPackageInfo.packageName = "pkg"; + testPackageInfo.applicationInfo = new ApplicationInfo(); + // restoreAnyVersion flag is off + testPackageInfo.applicationInfo.flags = 0; + + boolean eligibilityCriteria = mRestoreTask.isPackageEligibleForVToURestore(testPackageInfo); + + assertFalse(eligibilityCriteria); + } + + @Test + public void testIsEligibleForVToUDowngrade_pkgIsOnAllowlist_returnTrue() { + PackageInfo testPackageInfo = new PackageInfo(); + testPackageInfo.packageName = "pkg1"; + testPackageInfo.applicationInfo = new ApplicationInfo(); + // restoreAnyVersion flag is off + testPackageInfo.applicationInfo.flags = 0; + + boolean eligibilityCriteria = mRestoreTask.isPackageEligibleForVToURestore(testPackageInfo); + + assertTrue(eligibilityCriteria); + } + + @Test + public void testIsEligibleForVToUDowngrade_pkgIsNotOnDenyList_returnTrue() { + PackageInfo testPackageInfo = new PackageInfo(); + testPackageInfo.packageName = "pkg"; + testPackageInfo.applicationInfo = new ApplicationInfo(); + // restoreAnyVersion flag is on + testPackageInfo.applicationInfo.flags = ApplicationInfo.FLAG_RESTORE_ANY_VERSION; + + boolean eligibilityCriteria = mRestoreTask.isPackageEligibleForVToURestore(testPackageInfo); + + assertTrue(eligibilityCriteria); + } + + @Test + public void testIsEligibleForVToUDowngrade_pkgIsOnDenyList_returnFalse() { + PackageInfo testPackageInfo = new PackageInfo(); + testPackageInfo.packageName = "pkg2"; + testPackageInfo.applicationInfo = new ApplicationInfo(); + // restoreAnyVersion flag is on + testPackageInfo.applicationInfo.flags = ApplicationInfo.FLAG_RESTORE_ANY_VERSION; + + boolean eligibilityCriteria = mRestoreTask.isPackageEligibleForVToURestore(testPackageInfo); + + assertFalse(eligibilityCriteria); + } + private void setupForRestoreKeyValueState(int transportStatus) throws RemoteException, TransportNotAvailableException { // Mock BackupHandler to do nothing when executeNextState() is called diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java index 6cd79bc09fb6..ae6984ef0a8d 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java @@ -18,12 +18,17 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.Context; import android.hardware.SensorManager; +import android.os.AggregateBatteryConsumer; import android.os.BatteryConsumer; import android.os.BatteryManager; import android.os.BatteryStats; @@ -34,6 +39,7 @@ import android.os.Parcel; import android.os.Process; import android.os.UidBatteryConsumer; import android.platform.test.ravenwood.RavenwoodRule; +import android.util.SparseLongArray; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -517,6 +523,57 @@ public class BatteryUsageStatsProviderTest { } @Test + public void saveBatteryUsageStatsOnReset_incompatibleEnergyConsumers() throws Throwable { + MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); + batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"}); + int componentId0 = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; + int componentId1 = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1; + + synchronized (batteryStats) { + batteryStats.getUidStatsLocked(APP_UID); + + SparseLongArray uidEnergies = new SparseLongArray(); + uidEnergies.put(APP_UID, 30_000_000); + batteryStats.updateCustomEnergyConsumerStatsLocked(0, 100_000_000, uidEnergies); + batteryStats.updateCustomEnergyConsumerStatsLocked(1, 200_000_000, uidEnergies); + } + + BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null, + mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock); + + PowerStatsStore powerStatsStore = mock(PowerStatsStore.class); + doAnswer(invocation -> { + BatteryUsageStats stats = invocation.getArgument(1); + AggregateBatteryConsumer device = stats.getAggregateBatteryConsumer( + BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE); + assertThat(device.getCustomPowerComponentName(componentId0)).isEqualTo("FOO"); + assertThat(device.getCustomPowerComponentName(componentId1)).isEqualTo("BAR"); + assertThat(device.getConsumedPowerForCustomComponent(componentId0)) + .isWithin(PRECISION).of(27.77777); + assertThat(device.getConsumedPowerForCustomComponent(componentId1)) + .isWithin(PRECISION).of(55.55555); + + UidBatteryConsumer uid = stats.getUidBatteryConsumers().get(0); + assertThat(uid.getConsumedPowerForCustomComponent(componentId0)) + .isWithin(PRECISION).of(8.33333); + assertThat(uid.getConsumedPowerForCustomComponent(componentId1)) + .isWithin(PRECISION).of(8.33333); + return null; + }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any()); + + mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore); + + // Make an incompatible change of supported energy components. This will trigger + // a BatteryStats reset, which will generate a snapshot of battery stats. + mStatsRule.initMeasuredEnergyStatsLocked( + new String[]{"COMPONENT1"}); + + mStatsRule.waitForBackgroundThread(); + + verify(powerStatsStore).storeBatteryUsageStats(anyLong(), any()); + } + + @Test public void testAggregateBatteryStats_incompatibleSnapshot() { MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"}); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java index 8bdb0292bf00..7e8fa55020ab 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java @@ -16,6 +16,8 @@ package com.android.server.power.stats; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -28,6 +30,7 @@ import android.os.BatteryConsumer; import android.os.BatteryStats; import android.os.BatteryUsageStats; import android.os.BatteryUsageStatsQuery; +import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.UidBatteryConsumer; @@ -74,6 +77,7 @@ public class BatteryUsageStatsRule implements TestRule { private NetworkStats mNetworkStats; private boolean[] mSupportedStandardBuckets; private String[] mCustomPowerComponentNames; + private Throwable mThrowable; public BatteryUsageStatsRule() { this(0, null); @@ -270,6 +274,7 @@ public class BatteryUsageStatsRule implements TestRule { public void evaluate() throws Throwable { before(); base.evaluate(); + after(); } }; } @@ -277,6 +282,9 @@ public class BatteryUsageStatsRule implements TestRule { private void before() { lateInitBatteryStats(); HandlerThread bgThread = new HandlerThread("bg thread"); + bgThread.setUncaughtExceptionHandler((thread, throwable)-> { + mThrowable = throwable; + }); bgThread.start(); mHandler = new Handler(bgThread.getLooper()); mBatteryStats.setHandler(mHandler); @@ -285,6 +293,26 @@ public class BatteryUsageStatsRule implements TestRule { mBatteryStats.getOnBatteryScreenOffTimeBase().setRunning(!mScreenOn, 0, 0); } + private void after() throws Throwable { + if (mHandler != null) { + waitForBackgroundThread(); + } + } + + public void waitForBackgroundThread() throws Throwable { + if (mThrowable != null) { + throw mThrowable; + } + + ConditionVariable done = new ConditionVariable(); + mHandler.post(done::open); + assertThat(done.block(10000)).isTrue(); + + if (mThrowable != null) { + throw mThrowable; + } + } + public PowerProfile getPowerProfile() { return mPowerProfile; } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java index b322dd709c2d..aec3f451fac6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java @@ -17,6 +17,7 @@ package com.android.server.accessibility; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.anyInt; @@ -33,17 +34,24 @@ import android.testing.DexmakerShareClassLoaderRule; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.util.HexDump; + import com.google.common.truth.Expect; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.io.File; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; import java.util.List; /** @@ -51,184 +59,265 @@ import java.util.List; * * <p>Prefer adding new tests in CTS where possible. */ +@RunWith(Enclosed.class) public class BrailleDisplayConnectionTest { - private static final Path NULL_PATH = Path.of("/dev/null"); - - private BrailleDisplayConnection mBrailleDisplayConnection; - @Mock - private BrailleDisplayConnection.NativeInterface mNativeInterface; - @Mock - private AccessibilityServiceConnection mServiceConnection; - - @Rule - public final Expect expect = Expect.create(); - - private Context mContext; - - // To mock package-private class - @Rule - public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = - new DexmakerShareClassLoaderRule(); - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mContext = InstrumentationRegistry.getInstrumentation().getContext(); - when(mServiceConnection.isConnectedLocked()).thenReturn(true); - mBrailleDisplayConnection = - spy(new BrailleDisplayConnection(new Object(), mServiceConnection)); - } - @Test - public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception { - File testDir = mContext.getFilesDir(); - Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0"); - Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1"); - Path otherDevice = Path.of(testDir.getPath(), "otherDevice"); - Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice}; - try { - for (Path node : nodePaths) { - assertThat(node.toFile().createNewFile()).isTrue(); + public static class ScannerTest { + private static final Path NULL_PATH = Path.of("/dev/null"); + + private BrailleDisplayConnection mBrailleDisplayConnection; + @Mock + private BrailleDisplayConnection.NativeInterface mNativeInterface; + @Mock + private AccessibilityServiceConnection mServiceConnection; + + @Rule + public final Expect expect = Expect.create(); + + private Context mContext; + + // To mock package-private class + @Rule + public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = + new DexmakerShareClassLoaderRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + when(mServiceConnection.isConnectedLocked()).thenReturn(true); + mBrailleDisplayConnection = + spy(new BrailleDisplayConnection(new Object(), mServiceConnection)); + } + + @Test + public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception { + File testDir = mContext.getFilesDir(); + Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0"); + Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1"); + Path otherDevice = Path.of(testDir.getPath(), "otherDevice"); + Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice}; + try { + for (Path node : nodePaths) { + assertThat(node.toFile().createNewFile()).isTrue(); + } + + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + + assertThat(scanner.getHidrawNodePaths(testDir.toPath())) + .containsExactly(hidrawNode0, hidrawNode1); + } finally { + for (Path node : nodePaths) { + node.toFile().delete(); + } } + } + + @Test + public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() { + int descriptorSize = 4; + byte[] descriptor = {0xB, 0xE, 0xE, 0xF}; + when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize); + when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn( + descriptor); BrailleDisplayConnection.BrailleDisplayScanner scanner = mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - assertThat(scanner.getHidrawNodePaths(testDir.toPath())) - .containsExactly(hidrawNode0, hidrawNode1); - } finally { - for (Path node : nodePaths) { - node.toFile().delete(); - } + assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor); } - } - @Test - public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() { - int descriptorSize = 4; - byte[] descriptor = {0xB, 0xE, 0xE, 0xF}; - when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize); - when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor); + @Test + public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() { + when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0); - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor); - } + assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull(); + } - @Test - public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() { - when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0); + @Test + public void defaultNativeScanner_getUniqueId_returnsUniq() { + String macAddress = "12:34:56:78"; + when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress); - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull(); - } + assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress); + } + + @Test + public void defaultNativeScanner_getDeviceBusType_busUsb() { + when(mNativeInterface.getHidrawBusType(anyInt())) + .thenReturn(BrailleDisplayConnection.BUS_USB); - @Test - public void defaultNativeScanner_getUniqueId_returnsUniq() { - String macAddress = "12:34:56:78"; - when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress); + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + assertThat(scanner.getDeviceBusType(NULL_PATH)) + .isEqualTo(BrailleDisplayConnection.BUS_USB); + } - assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress); - } + @Test + public void defaultNativeScanner_getDeviceBusType_busBluetooth() { + when(mNativeInterface.getHidrawBusType(anyInt())) + .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH); - @Test - public void defaultNativeScanner_getDeviceBusType_busUsb() { - when(mNativeInterface.getHidrawBusType(anyInt())) - .thenReturn(BrailleDisplayConnection.BUS_USB); + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + assertThat(scanner.getDeviceBusType(NULL_PATH)) + .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH); + } - assertThat(scanner.getDeviceBusType(NULL_PATH)) - .isEqualTo(BrailleDisplayConnection.BUS_USB); - } + @Test + public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() { + Mockito.doNothing().when(mBrailleDisplayConnection).disconnect(); + mBrailleDisplayConnection.write( + new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]); - @Test - public void defaultNativeScanner_getDeviceBusType_busBluetooth() { - when(mNativeInterface.getHidrawBusType(anyInt())) - .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH); + verify(mBrailleDisplayConnection).disconnect(); + } - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + @Test + public void write_notConnected_throwsIllegalStateException() { + when(mServiceConnection.isConnectedLocked()).thenReturn(false); - assertThat(scanner.getDeviceBusType(NULL_PATH)) - .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH); - } + assertThrows(IllegalStateException.class, + () -> mBrailleDisplayConnection.write(new byte[1])); + } - @Test - public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() { - Mockito.doNothing().when(mBrailleDisplayConnection).disconnect(); - mBrailleDisplayConnection.write( - new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]); + @Test + public void write_unableToCreateWriteStream_disconnects() { + Mockito.doNothing().when(mBrailleDisplayConnection).disconnect(); + // mBrailleDisplayConnection#connectLocked was never called so the + // connection's mHidrawNode is still null. This will throw an exception + // when attempting to create FileOutputStream on the node. + mBrailleDisplayConnection.write(new byte[1]); - verify(mBrailleDisplayConnection).disconnect(); - } + verify(mBrailleDisplayConnection).disconnect(); + } - @Test - public void write_notConnected_throwsIllegalStateException() { - when(mServiceConnection.isConnectedLocked()).thenReturn(false); + // BrailleDisplayConnection#setTestData() is used to enable CTS testing with + // test Braille display data, but its own implementation should also be tested + // so that issues in this helper don't cause confusing failures in CTS. + + @Test + public void setTestData_scannerReturnsTestData() { + Bundle bd1 = new Bundle(), bd2 = new Bundle(); + + Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2"); + bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, + path1.toString()); + bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, + path2.toString()); + byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF}; + bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1); + bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2); + String uniq1 = "uniq1", uniq2 = "uniq2"; + bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1); + bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2); + int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = + BrailleDisplayConnection.BUS_BLUETOOTH; + bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, + bus1 == BrailleDisplayConnection.BUS_BLUETOOTH); + bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, + bus2 == BrailleDisplayConnection.BUS_BLUETOOTH); - assertThrows(IllegalStateException.class, - () -> mBrailleDisplayConnection.write(new byte[1])); - } + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.setTestData(List.of(bd1, bd2)); + + expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2); + expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1); + expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2); + expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1); + expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2); + expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1); + expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2); + } - @Test - public void write_unableToCreateWriteStream_disconnects() { - Mockito.doNothing().when(mBrailleDisplayConnection).disconnect(); - // mBrailleDisplayConnection#connectLocked was never called so the - // connection's mHidrawNode is still null. This will throw an exception - // when attempting to create FileOutputStream on the node. - mBrailleDisplayConnection.write(new byte[1]); + @Test + public void setTestData_emptyTestData_returnsNullNodePaths() { + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.setTestData(List.of()); - verify(mBrailleDisplayConnection).disconnect(); + expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull(); + } } - // BrailleDisplayConnection#setTestData() is used to enable CTS testing with - // test Braille display data, but its own implementation should also be tested - // so that issues in this helper don't cause confusing failures in CTS. - - @Test - public void setTestData_scannerReturnsTestData() { - Bundle bd1 = new Bundle(), bd2 = new Bundle(); - - Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2"); - bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString()); - bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString()); - byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF}; - bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1); - bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2); - String uniq1 = "uniq1", uniq2 = "uniq2"; - bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1); - bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2); - int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH; - bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, - bus1 == BrailleDisplayConnection.BUS_BLUETOOTH); - bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, - bus2 == BrailleDisplayConnection.BUS_BLUETOOTH); - - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.setTestData(List.of(bd1, bd2)); - - expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2); - expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1); - expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2); - expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1); - expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2); - expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1); - expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2); - } + @RunWith(Parameterized.class) + public static class BrailleDisplayDescriptorTest { + @Parameterized.Parameters(name = "{0}") + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][]{ + {"match_BdPage", new byte[]{ + // Just one item, defines the BD page + 0x05, 0x41}}, + {"match_BdPageAfterAnotherPage", new byte[]{ + // One item defines another page + 0x05, 0x01, + // Next item defines BD page + 0x05, 0x41}}, + {"match_BdPageAfterSizeZeroItem", new byte[]{ + // Size-zero item (last 2 bits are 00) + 0x00, + // Next item defines BD page + 0x05, 0x41}}, + {"match_BdPageAfterSizeOneItem", new byte[]{ + // Size-one item (last 2 bits are 01) + 0x01, 0x7F, + // Next item defines BD page + 0x05, 0x41}}, + {"match_BdPageAfterSizeTwoItem", new byte[]{ + // Size-two item (last 2 bits are 10) + 0x02, 0x7F, 0x7F, + 0x05, 0x41}}, + {"match_BdPageAfterSizeFourItem", new byte[]{ + // Size-four item (last 2 bits are 11) + 0x03, 0x7F, 0x7F, 0x7F, 0x7F, + 0x05, 0x41}}, + {"match_BdPageInBetweenOtherPages", new byte[]{ + // One item defines another page + 0x05, 0x01, + // Next item defines BD page + 0x05, 0x41, + // Next item defines another page + 0x05, 0x02}}, + {"fail_OtherPage", new byte[]{ + // Just one item, defines another page + 0x05, 0x01}}, + {"fail_BdPageBeforeMissingData", new byte[]{ + // This item defines BD page + 0x05, 0x41, + // Next item specifies size-one item (last 2 bits are 01) but + // that one data byte is missing; this descriptor is malformed. + 0x01}}, + {"fail_BdPageWithWrongDataSize", new byte[]{ + // This item defines a page with two-byte ID 0x41 0x7F, not 0x41. + 0x06, 0x41, 0x7F}}, + {"fail_LongItem", new byte[]{ + // Item has type bits 1111, indicating Long Item. + (byte) 0xF0}}, + }); + } - @Test - public void setTestData_emptyTestData_returnsNullNodePaths() { - BrailleDisplayConnection.BrailleDisplayScanner scanner = - mBrailleDisplayConnection.setTestData(List.of()); - expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull(); + @Parameterized.Parameter(0) + public String mTestName; + @Parameterized.Parameter(1) + public byte[] mDescriptor; + + @Test + public void isBrailleDisplay() { + final boolean expectedMatch = mTestName.startsWith("match_"); + assertWithMessage( + "Expected isBrailleDisplay==" + expectedMatch + + " for descriptor " + HexDump.toHexString(mDescriptor)) + .that(BrailleDisplayConnection.isBrailleDisplay(mDescriptor)) + .isEqualTo(expectedMatch); + } } } diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 81df597f3f33..3e748ffb37e9 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -75,6 +75,7 @@ import android.content.pm.SigningDetails; import android.content.pm.SigningInfo; import android.content.pm.UserInfo; import android.content.pm.UserPackage; +import android.content.pm.UserProperties; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.drawable.Icon; @@ -776,6 +777,15 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { new UserInfo(USER_P1, "userP1", UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_MANAGED_PROFILE), 0); + protected static final UserProperties USER_PROPERTIES_0 = + new UserProperties.Builder().setItemsRestrictedOnHomeScreen(false).build(); + + protected static final UserProperties USER_PROPERTIES_10 = + new UserProperties.Builder().setItemsRestrictedOnHomeScreen(false).build(); + + protected static final UserProperties USER_PROPERTIES_11 = + new UserProperties.Builder().setItemsRestrictedOnHomeScreen(true).build(); + protected BiPredicate<String, Integer> mDefaultLauncherChecker = (callingPackage, userId) -> LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage) @@ -817,6 +827,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { = new HashMap<>(); protected final Map<Integer, UserInfo> mUserInfos = new HashMap<>(); + protected final Map<Integer, UserProperties> mUserProperties = new HashMap<>(); protected final Map<Integer, Boolean> mRunningUsers = new HashMap<>(); protected final Map<Integer, Boolean> mUnlockedUsers = new HashMap<>(); @@ -911,6 +922,9 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { mUserInfos.put(USER_11, USER_INFO_11); mUserInfos.put(USER_P0, USER_INFO_P0); mUserInfos.put(USER_P1, USER_INFO_P1); + mUserProperties.put(USER_0, USER_PROPERTIES_0); + mUserProperties.put(USER_10, USER_PROPERTIES_10); + mUserProperties.put(USER_11, USER_PROPERTIES_11); when(mMockUserManagerInternal.isUserUnlockingOrUnlocked(anyInt())) .thenAnswer(inv -> { @@ -959,6 +973,15 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { inv -> mUserInfos.get((Integer) inv.getArguments()[0]))); when(mMockActivityManagerInternal.getUidProcessState(anyInt())).thenReturn( ActivityManager.PROCESS_STATE_CACHED_EMPTY); + when(mMockUserManagerInternal.getUserProperties(anyInt())) + .thenAnswer(inv -> { + final int userId = (Integer) inv.getArguments()[0]; + final UserProperties userProperties = mUserProperties.get(userId); + if (userProperties == null) { + return new UserProperties.Builder().build(); + } + return userProperties; + }); // User 0 and P0 are always running mRunningUsers.put(USER_0, true); diff --git a/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java b/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java index f5c6795484fa..771a76517b22 100644 --- a/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java +++ b/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java @@ -92,7 +92,7 @@ public class SearchablesTest { public void testNonSearchable() { // test basic array & hashmap Searchables searchables = new Searchables(mContext, 0); - searchables.updateSearchableList(); + searchables.updateSearchableListIfNeeded(); // confirm that we return null for non-searchy activities ComponentName nonActivity = new ComponentName("com.android.frameworks.servicestests", @@ -121,7 +121,7 @@ public class SearchablesTest { doReturn(true).when(mPackageManagerInternal).canAccessComponent(anyInt(), any(), anyInt()); Searchables searchables = new Searchables(mContext, 0); - searchables.updateSearchableList(); + searchables.updateSearchableListIfNeeded(); // tests with "real" searchables (deprecate, this should be a unit test) ArrayList<SearchableInfo> searchablesList = searchables.getSearchablesList(); int count = searchablesList.size(); @@ -139,7 +139,7 @@ public class SearchablesTest { doReturn(false).when(mPackageManagerInternal).canAccessComponent(anyInt(), any(), anyInt()); Searchables searchables = new Searchables(mContext, 0); - searchables.updateSearchableList(); + searchables.updateSearchableListIfNeeded(); ArrayList<SearchableInfo> searchablesList = searchables.getSearchablesList(); assertNotNull(searchablesList); MoreAsserts.assertEmpty(searchablesList); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index cff7f460feb5..715c9d4081b2 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -12008,7 +12008,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // style + self managed call - bypasses block when(mTelecomManager.isInSelfManagedCall( - r.getSbn().getPackageName(), r.getUser(), true)).thenReturn(true); + r.getSbn().getPackageName(), true)).thenReturn(true); assertThat(mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(), r.getSbn().getId(), r.getSbn().getTag(), r, false, false)).isTrue(); @@ -12091,7 +12091,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // style + self managed call - bypasses block mService.clearNotifications(); reset(mUsageStats); - when(mTelecomManager.isInSelfManagedCall(r.getSbn().getPackageName(), r.getUser(), true)) + when(mTelecomManager.isInSelfManagedCall(r.getSbn().getPackageName(), true)) .thenReturn(true); mService.addEnqueuedNotification(r); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java index b431888a72fb..3e59878f9e1e 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java @@ -35,8 +35,8 @@ import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContextWrapper; import android.content.pm.PackageManagerInternal; +import android.os.ExternalVibrationScale; import android.os.Handler; -import android.os.IExternalVibratorService; import android.os.PowerManagerInternal; import android.os.UserHandle; import android.os.VibrationAttributes; @@ -49,6 +49,7 @@ import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; +import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -119,29 +120,65 @@ public class VibrationScalerTest { public void testGetExternalVibrationScale() { setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH); - assertEquals(IExternalVibratorService.SCALE_VERY_HIGH, - mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH)); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_VERY_HIGH, + mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH)); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM); - assertEquals(IExternalVibratorService.SCALE_HIGH, - mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH)); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_HIGH, + mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH)); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_LOW); - assertEquals(IExternalVibratorService.SCALE_NONE, - mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH)); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_NONE, + mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH)); setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_MEDIUM); - assertEquals(IExternalVibratorService.SCALE_LOW, - mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH)); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_LOW, + mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH)); setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH); - assertEquals(IExternalVibratorService.SCALE_VERY_LOW, - mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH)); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_VERY_LOW, + mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH)); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); // Vibration setting being bypassed will use default setting and not scale. - assertEquals(IExternalVibratorService.SCALE_NONE, - mVibrationScaler.getExternalVibrationScale(USAGE_TOUCH)); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_NONE, + mVibrationScaler.getExternalVibrationScaleLevel(USAGE_TOUCH)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + public void testAdaptiveHapticsScale_withAdaptiveHapticsAvailable() { + setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW); + setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW); + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH); + setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH); + + mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f); + mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f); + + assertEquals(0.5f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_TOUCH)); + assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE)); + assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_NOTIFICATION)); + + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); + // Vibration setting being bypassed will apply adaptive haptics scales. + assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE)); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + public void testAdaptiveHapticsScale_flagDisabled_adaptiveHapticScaleAlwaysNone() { + setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW); + setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW); + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH); + setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH); + + mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f); + mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f); + + assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_TOUCH)); + assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE)); + assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_NOTIFICATION)); } @Test diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java index 2823223e4859..417fbd06be66 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java @@ -1,5 +1,5 @@ /* - * 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. @@ -35,7 +35,6 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.pm.PackageManagerInternal; import android.frameworks.vibrator.ScaleParam; -import android.frameworks.vibrator.VibrationParam; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -55,8 +54,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -135,7 +132,7 @@ public class VibratorControlServiceTest { vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f); mVibratorControlService.onRequestVibrationParamsComplete(token, - generateVibrationParams(vibrationScales)); + VibrationParamGenerator.generateVibrationParams(vibrationScales)); verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_ALARM, 0.7f); verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.4f); @@ -162,7 +159,7 @@ public class VibratorControlServiceTest { vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f); mVibratorControlService.onRequestVibrationParamsComplete(new Binder(), - generateVibrationParams(vibrationScales)); + VibrationParamGenerator.generateVibrationParams(vibrationScales)); verifyZeroInteractions(mMockVibrationScaler); } @@ -175,7 +172,8 @@ public class VibratorControlServiceTest { vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f); vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f); - mVibratorControlService.setVibrationParams(generateVibrationParams(vibrationScales), + mVibratorControlService.setVibrationParams( + VibrationParamGenerator.generateVibrationParams(vibrationScales), mFakeVibratorController); verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_ALARM, 0.7f); @@ -193,7 +191,8 @@ public class VibratorControlServiceTest { vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f); vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f); - mVibratorControlService.setVibrationParams(generateVibrationParams(vibrationScales), + mVibratorControlService.setVibrationParams( + VibrationParamGenerator.generateVibrationParams(vibrationScales), mFakeVibratorController); verifyZeroInteractions(mMockVibrationScaler); @@ -268,28 +267,6 @@ public class VibratorControlServiceTest { } } - private VibrationParam[] generateVibrationParams(SparseArray<Float> vibrationScales) { - List<VibrationParam> vibrationParamList = new ArrayList<>(); - for (int i = 0; i < vibrationScales.size(); i++) { - int type = vibrationScales.keyAt(i); - float scale = vibrationScales.valueAt(i); - - vibrationParamList.add(generateVibrationParam(type, scale)); - } - - return vibrationParamList.toArray(new VibrationParam[0]); - } - - private VibrationParam generateVibrationParam(int type, float scale) { - ScaleParam scaleParam = new ScaleParam(); - scaleParam.typesMask = type; - scaleParam.scale = scale; - VibrationParam vibrationParam = new VibrationParam(); - vibrationParam.setScale(scaleParam); - - return vibrationParam; - } - private int buildVibrationTypesMask(int... types) { int typesMask = 0; for (int type : types) { diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index e7571ef47864..ed89ccf07453 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -50,6 +50,7 @@ import android.content.ContextWrapper; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.res.Resources; +import android.frameworks.vibrator.ScaleParam; import android.hardware.input.IInputManager; import android.hardware.input.InputManager; import android.hardware.input.InputManagerGlobal; @@ -59,16 +60,17 @@ import android.media.AudioAttributes; import android.media.AudioManager; import android.os.CombinedVibration; import android.os.ExternalVibration; +import android.os.ExternalVibrationScale; import android.os.Handler; import android.os.IBinder; import android.os.IExternalVibrationController; -import android.os.IExternalVibratorService; import android.os.IVibratorStateListener; import android.os.Looper; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.Process; +import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.VibrationAttributes; @@ -82,6 +84,10 @@ import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.util.SparseArray; @@ -153,6 +159,8 @@ public class VibratorManagerServiceTest { public MockitoRule rule = MockitoJUnit.rule(); @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -185,8 +193,10 @@ public class VibratorManagerServiceTest { private Context mContextSpy; private TestLooper mTestLooper; private FakeVibrator mVibrator; + private FakeVibratorController mFakeVibratorController; private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener; private VibratorManagerService.ExternalVibratorService mExternalVibratorService; + private VibratorControlService mVibratorControlService; private VibrationConfig mVibrationConfig; private InputManagerGlobal.TestSession mInputManagerGlobalSession; private InputManager mInputManager; @@ -197,6 +207,7 @@ public class VibratorManagerServiceTest { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext())); mInputManagerGlobalSession = InputManagerGlobal.createTestSession(mIInputManagerMock); mVibrationConfig = new VibrationConfig(mContextSpy.getResources()); + mFakeVibratorController = new FakeVibratorController(); ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy); when(mContextSpy.getContentResolver()).thenReturn(contentResolver); @@ -310,6 +321,8 @@ public class VibratorManagerServiceTest { if (service instanceof VibratorManagerService.ExternalVibratorService) { mExternalVibratorService = (VibratorManagerService.ExternalVibratorService) service; + } else if (service instanceof VibratorControlService) { + mVibratorControlService = (VibratorControlService) service; } } @@ -321,9 +334,13 @@ public class VibratorManagerServiceTest { VibratorControllerHolder createVibratorControllerHolder() { VibratorControllerHolder holder = new VibratorControllerHolder(); - holder.setVibratorController(new FakeVibratorController()); + holder.setVibratorController(mFakeVibratorController); return holder; } + + boolean isServiceDeclared(String name) { + return true; + } }); return mService; } @@ -1108,12 +1125,13 @@ public class VibratorManagerServiceTest { ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ALARM_ATTRS, controller, firstToken); - int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), RINGTONE_ATTRS); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // The external vibration should have been cancelled verify(controller).mute(); assertEquals(Arrays.asList(false, true, false), @@ -1708,13 +1726,14 @@ public class VibratorManagerServiceTest { ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class), binderToken); - int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); + ExternalVibrationScale scale = mExternalVibratorService.onExternalVibrationStart( + externalVibration); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); when(mVirtualDeviceManagerInternalMock.isAppRunningOnAnyVirtualDevice(UID)) .thenReturn(true); scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); - assertEquals(IExternalVibratorService.SCALE_MUTE, scale); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); } @Test @@ -1727,10 +1746,11 @@ public class VibratorManagerServiceTest { ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class), binderToken); - int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); + ExternalVibrationScale scale = mExternalVibratorService.onExternalVibrationStart( + externalVibration); mExternalVibratorService.onExternalVibrationStop(externalVibration); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); assertEquals(Arrays.asList(false, true, false), mVibratorProviders.get(1).getExternalControlStates()); @@ -1753,17 +1773,19 @@ public class VibratorManagerServiceTest { ExternalVibration firstVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ALARM_ATTRS, firstController, firstToken); - int firstScale = mExternalVibratorService.onExternalVibrationStart(firstVibration); + ExternalVibrationScale firstScale = + mExternalVibratorService.onExternalVibrationStart(firstVibration); AudioAttributes ringtoneAudioAttrs = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .build(); ExternalVibration secondVibration = new ExternalVibration(UID, PACKAGE_NAME, ringtoneAudioAttrs, secondController, secondToken); - int secondScale = mExternalVibratorService.onExternalVibrationStart(secondVibration); + ExternalVibrationScale secondScale = + mExternalVibratorService.onExternalVibrationStart(secondVibration); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, firstScale); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, secondScale); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, firstScale.scaleLevel); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, secondScale.scaleLevel); verify(firstController).mute(); verify(secondController, never()).mute(); // Set external control called only once. @@ -1799,8 +1821,9 @@ public class VibratorManagerServiceTest { ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class)); - int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // Vibration is cancelled. assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); @@ -1825,9 +1848,10 @@ public class VibratorManagerServiceTest { ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class)); - int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); // External vibration is ignored. - assertEquals(IExternalVibratorService.SCALE_MUTE, scale); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // Vibration is not cancelled. assertFalse(waitUntil(s -> !s.isVibrating(1), service, CLEANUP_TIMEOUT_MILLIS)); @@ -1852,8 +1876,9 @@ public class VibratorManagerServiceTest { ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class)); - int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // Vibration is cancelled. assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); @@ -1879,9 +1904,10 @@ public class VibratorManagerServiceTest { ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_NOTIFICATION_ATTRS, mock(IExternalVibrationController.class)); - int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); // New vibration is ignored. - assertEquals(IExternalVibratorService.SCALE_MUTE, scale); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // Vibration is not cancelled. assertFalse(waitUntil(s -> !s.isVibrating(1), service, CLEANUP_TIMEOUT_MILLIS)); @@ -1901,18 +1927,19 @@ public class VibratorManagerServiceTest { setRingerMode(AudioManager.RINGER_MODE_SILENT); createSystemReadyService(); - int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); - assertEquals(IExternalVibratorService.SCALE_MUTE, scale); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); setRingerMode(AudioManager.RINGER_MODE_NORMAL); createSystemReadyService(); scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); setRingerMode(AudioManager.RINGER_MODE_VIBRATE); createSystemReadyService(); scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); } @Test @@ -1935,14 +1962,14 @@ public class VibratorManagerServiceTest { ExternalVibration vib = new ExternalVibration(UID, PACKAGE_NAME, audioAttrs, mock(IExternalVibrationController.class)); - int scale = mExternalVibratorService.onExternalVibrationStart(vib); - assertEquals(IExternalVibratorService.SCALE_MUTE, scale); + ExternalVibrationScale scale = mExternalVibratorService.onExternalVibrationStart(vib); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); mExternalVibratorService.onExternalVibrationStop(vib); scale = mExternalVibratorService.onExternalVibrationStart( new ExternalVibration(UID, PACKAGE_NAME, flaggedAudioAttrs, mock(IExternalVibrationController.class))); - assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); } @Test @@ -1956,10 +1983,91 @@ public class VibratorManagerServiceTest { .build(); createSystemReadyService(); - int scale = mExternalVibratorService.onExternalVibrationStart( - new ExternalVibration(/* uid= */ 123, PACKAGE_NAME, flaggedAudioAttrs, - mock(IExternalVibrationController.class))); - assertEquals(IExternalVibratorService.SCALE_MUTE, scale); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart( + new ExternalVibration(/* uid= */ 123, PACKAGE_NAME, flaggedAudioAttrs, + mock(IExternalVibrationController.class))); + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); + } + + @Test + @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + public void onExternalVibration_withAdaptiveHaptics_returnsCorrectAdaptiveScales() + throws RemoteException { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL, + IVibrator.CAP_AMPLITUDE_CONTROL); + createSystemReadyService(); + + SparseArray<Float> vibrationScales = new SparseArray<>(); + vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f); + vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f); + + mVibratorControlService.setVibrationParams( + VibrationParamGenerator.generateVibrationParams(vibrationScales), + mFakeVibratorController); + ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_ALARM_ATTRS, + mock(IExternalVibrationController.class)); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + mExternalVibratorService.onExternalVibrationStop(externalVibration); + + assertEquals(scale.adaptiveHapticsScale, 0.7f, 0); + + externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_NOTIFICATION_ATTRS, + mock(IExternalVibrationController.class)); + scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); + mExternalVibratorService.onExternalVibrationStop(externalVibration); + + assertEquals(scale.adaptiveHapticsScale, 0.4f, 0); + + AudioAttributes ringtoneAudioAttrs = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build(); + externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + ringtoneAudioAttrs, + mock(IExternalVibrationController.class)); + scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); + + assertEquals(scale.adaptiveHapticsScale, 1f, 0); + } + + @Test + @RequiresFlagsDisabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + public void onExternalVibration_withAdaptiveHapticsFlagDisabled_alwaysReturnScaleNone() + throws RemoteException { + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL, + IVibrator.CAP_AMPLITUDE_CONTROL); + createSystemReadyService(); + + SparseArray<Float> vibrationScales = new SparseArray<>(); + vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f); + vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f); + + mVibratorControlService.setVibrationParams( + VibrationParamGenerator.generateVibrationParams(vibrationScales), + mFakeVibratorController); + ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_ALARM_ATTRS, + mock(IExternalVibrationController.class)); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + mExternalVibratorService.onExternalVibrationStop(externalVibration); + + assertEquals(scale.adaptiveHapticsScale, 1f, 0); + + mVibratorControlService.setVibrationParams( + VibrationParamGenerator.generateVibrationParams(vibrationScales), + mFakeVibratorController); + externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_NOTIFICATION_ATTRS, + mock(IExternalVibrationController.class)); + scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); + + assertEquals(scale.adaptiveHapticsScale, 1f, 0); } @Test diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/VibrationParamGenerator.java b/services/tests/vibrator/utils/com/android/server/vibrator/VibrationParamGenerator.java new file mode 100644 index 000000000000..a606388da190 --- /dev/null +++ b/services/tests/vibrator/utils/com/android/server/vibrator/VibrationParamGenerator.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vibrator; + +import android.frameworks.vibrator.ScaleParam; +import android.frameworks.vibrator.VibrationParam; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * A helper class that can be used to generate arrays of {@link VibrationParam}. + */ +public final class VibrationParamGenerator { + /** + * Generates an array of {@link VibrationParam}. + */ + public static VibrationParam[] generateVibrationParams(SparseArray<Float> vibrationScales) { + List<VibrationParam> vibrationParamList = new ArrayList<>(); + for (int i = 0; i < vibrationScales.size(); i++) { + int type = vibrationScales.keyAt(i); + float scale = vibrationScales.valueAt(i); + + vibrationParamList.add(generateVibrationParam(type, scale)); + } + + return vibrationParamList.toArray(new VibrationParam[0]); + } + + private static VibrationParam generateVibrationParam(int type, float scale) { + ScaleParam scaleParam = new ScaleParam(); + scaleParam.typesMask = type; + scaleParam.scale = scale; + VibrationParam vibrationParam = new VibrationParam(); + vibrationParam.setScale(scaleParam); + + return vibrationParam; + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java index d84620b4444a..ead36f1d353f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.platform.test.annotations.Presubmit; +import android.server.wm.BuildUtils; import android.view.SurfaceControl; import android.window.SurfaceSyncGroup; @@ -370,6 +371,7 @@ public class SurfaceSyncGroupTest { assertEquals(0, finishedLatch.getCount()); } + @Test public void testSurfaceSyncGroupTimeout() throws InterruptedException { final CountDownLatch finishedLatch = new CountDownLatch(1); SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG); @@ -386,7 +388,7 @@ public class SurfaceSyncGroupTest { // Never finish syncTarget2 so it forces the timeout. Timeout is 1 second so wait a little // over 1 second to make sure it completes. - finishedLatch.await(1100, TimeUnit.MILLISECONDS); + finishedLatch.await(1100L * BuildUtils.HW_TIMEOUT_MULTIPLIER, TimeUnit.MILLISECONDS); assertEquals(0, finishedLatch.getCount()); } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 889f8429077c..c217780d90d6 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -2582,7 +2582,6 @@ public class VoiceInteractionManagerService extends SystemService { if (anyPackagesAppearing()) { initRecognizer(userHandle); } - return; } if (curInteractor != null) { @@ -2631,15 +2630,16 @@ public class VoiceInteractionManagerService extends SystemService { } } - // There is no interactor, so just deal with a simple recognizer. - int change = isPackageDisappearing(curRecognizer.getPackageName()); - if (change == PACKAGE_PERMANENT_CHANGE - || change == PACKAGE_TEMPORARY_CHANGE) { - setCurRecognizer(findAvailRecognizer(null, userHandle), userHandle); + if (curRecognizer != null) { + int change = isPackageDisappearing(curRecognizer.getPackageName()); + if (change == PACKAGE_PERMANENT_CHANGE + || change == PACKAGE_TEMPORARY_CHANGE) { + setCurRecognizer(findAvailRecognizer(null, userHandle), userHandle); - } else if (isPackageModified(curRecognizer.getPackageName())) { - setCurRecognizer(findAvailRecognizer(curRecognizer.getPackageName(), - userHandle), userHandle); + } else if (isPackageModified(curRecognizer.getPackageName())) { + setCurRecognizer(findAvailRecognizer(curRecognizer.getPackageName(), + userHandle), userHandle); + } } } } diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 08c76af70511..9792cdd80a00 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -2797,13 +2797,10 @@ public class TelecomManager { * calls for a given {@code packageName} and {@code userHandle}. * * @param packageName the package name of the app to check calls for. - * @param userHandle the user handle on which to check for calls. - * @param detectForAllUsers indicates if calls should be detected across all users. If it is - * set to {@code true}, and the caller has the ability to interact - * across users, the userHandle parameter is disregarded. + * @param userHandle the user handle to check calls for. * @return {@code true} if there are ongoing calls, {@code false} otherwise. - * @throws SecurityException if detectForAllUsers is true or userHandle is not the calling user - * and the caller does not grant the ability to interact across users. + * @throws SecurityException if the userHandle is not the calling user and the caller does not + * grant the ability to interact across users. * @hide */ @SystemApi @@ -2811,11 +2808,45 @@ public class TelecomManager { @RequiresPermission(allOf = {Manifest.permission.READ_PRIVILEGED_PHONE_STATE, Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) public boolean isInSelfManagedCall(@NonNull String packageName, - @NonNull UserHandle userHandle, boolean detectForAllUsers) { + @NonNull UserHandle userHandle) { ITelecomService service = getTelecomService(); if (service != null) { try { return service.isInSelfManagedCall(packageName, userHandle, + mContext.getOpPackageName(), false); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException isInSelfManagedCall: " + e); + e.rethrowFromSystemServer(); + return false; + } + } else { + throw new IllegalStateException("Telecom service is not present"); + } + } + + /** + * Determines whether there are any ongoing {@link PhoneAccount#CAPABILITY_SELF_MANAGED} + * calls for a given {@code packageName} amongst all users, given that detectForAllUsers is true + * and the caller has the ability to interact across users. If detectForAllUsers isn't enabled, + * the calls will be checked against the caller. + * + * @param packageName the package name of the app to check calls for. + * @param detectForAllUsers indicates if calls should be detected across all users. + * @return {@code true} if there are ongoing calls, {@code false} otherwise. + * @throws SecurityException if detectForAllUsers is true and the caller does not grant the + * ability to interact across users. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + @RequiresPermission(allOf = {Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) + public boolean isInSelfManagedCall(@NonNull String packageName, + boolean detectForAllUsers) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.isInSelfManagedCall(packageName, null, mContext.getOpPackageName(), detectForAllUsers); } catch (RemoteException e) { Log.e(TAG, "RemoteException isInSelfManagedCall: " + e); diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java index 5af2c3458368..55245419c570 100644 --- a/telephony/java/android/service/euicc/EuiccService.java +++ b/telephony/java/android/service/euicc/EuiccService.java @@ -856,10 +856,22 @@ public abstract class EuiccService extends Service { int slotId, IGetAvailableMemoryInBytesCallback callback) { mExecutor.execute( () -> { - long availableMemoryInBytes = - EuiccService.this.onGetAvailableMemoryInBytes(slotId); + long availableMemoryInBytes = EuiccManager.EUICC_MEMORY_FIELD_UNAVAILABLE; + String unsupportedOperationMessage = ""; try { - callback.onSuccess(availableMemoryInBytes); + availableMemoryInBytes = + EuiccService.this.onGetAvailableMemoryInBytes(slotId); + } catch (UnsupportedOperationException e) { + unsupportedOperationMessage = e.getMessage(); + } + + try { + if (!unsupportedOperationMessage.isEmpty()) { + callback.onUnsupportedOperationException( + unsupportedOperationMessage); + } else { + callback.onSuccess(availableMemoryInBytes); + } } catch (RemoteException e) { // Can't communicate with the phone process; ignore. } diff --git a/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl index bd6d19b81d47..e550e77a3605 100644 --- a/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl +++ b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl @@ -19,4 +19,5 @@ package android.service.euicc; /** @hide */ oneway interface IGetAvailableMemoryInBytesCallback { void onSuccess(long availableMemoryInBytes); + void onUnsupportedOperationException(String message); } |