diff options
128 files changed, 3981 insertions, 800 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 96315ebccc49..50d97cf0626f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -14435,6 +14435,7 @@ package android.telephony { method @NonNull public android.telephony.CarrierRestrictionRules build(); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setAllCarriersAllowed(); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setAllowedCarriers(@NonNull java.util.List<android.service.carrier.CarrierIdentifier>); + method @FlaggedApi("com.android.internal.telephony.flags.set_carrier_restriction_status") @NonNull public android.telephony.CarrierRestrictionRules.Builder setCarrierRestrictionStatus(int); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setDefaultCarrierRestriction(int); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setExcludedCarriers(@NonNull java.util.List<android.service.carrier.CarrierIdentifier>); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setMultiSimPolicy(int); diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index fd9600c1f87f..65628d32e583 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -16,6 +16,7 @@ package android.accessibilityservice; +import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION; import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; @@ -69,6 +70,8 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityWindowInfo; import android.view.inputmethod.EditorInfo; +import androidx.annotation.GuardedBy; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.inputmethod.IAccessibilityInputMethodSession; @@ -640,6 +643,8 @@ public abstract class AccessibilityService extends Service { /** The detected gesture information for different displays */ boolean onGesture(AccessibilityGestureEvent gestureInfo); boolean onKeyEvent(KeyEvent event); + /** Magnification SystemUI connection changed callbacks */ + void onMagnificationSystemUIConnectionChanged(boolean connected); /** Magnification changed callbacks for different displays */ void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config); @@ -790,7 +795,6 @@ public abstract class AccessibilityService extends Service { public static final String KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP = "screenshot_timestamp"; - /** * Annotations for result codes of attaching accessibility overlays. * @@ -837,6 +841,13 @@ public abstract class AccessibilityService extends Service { private WindowManager mWindowManager; + @GuardedBy("mLock") + private boolean mServiceConnected; + @GuardedBy("mLock") + private boolean mMagnificationSystemUIConnected; + @GuardedBy("mLock") + private boolean mServiceConnectedNotified; + /** List of magnification controllers, mapping from displayId -> MagnificationController. */ private final SparseArray<MagnificationController> mMagnificationControllers = new SparseArray<>(0); @@ -886,11 +897,14 @@ public abstract class AccessibilityService extends Service { for (int i = 0; i < mMagnificationControllers.size(); i++) { mMagnificationControllers.valueAt(i).onServiceConnectedLocked(); } + checkIsMagnificationSystemUIConnectedAlready(); final AccessibilityServiceInfo info = getServiceInfo(); if (info != null) { updateInputMethod(info); mMotionEventSources = info.getMotionEventSources(); } + mServiceConnected = true; + mServiceConnectedNotified = false; } if (mSoftKeyboardController != null) { mSoftKeyboardController.onServiceConnected(); @@ -898,7 +912,57 @@ public abstract class AccessibilityService extends Service { // The client gets to handle service connection last, after we've set // up any state upon which their code may rely. - onServiceConnected(); + if (android.view.accessibility.Flags + .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) { + notifyOnServiceConnectedIfReady(); + } else { + onServiceConnected(); + } + } + + private void notifyOnServiceConnectedIfReady() { + synchronized (mLock) { + if (mServiceConnectedNotified) { + return; + } + boolean canControlMagnification; + final AccessibilityServiceInfo info = getServiceInfo(); + if (info != null) { + int flagMask = CAPABILITY_CAN_CONTROL_MAGNIFICATION; + canControlMagnification = (info.getCapabilities() & flagMask) == flagMask; + } else { + canControlMagnification = false; + } + boolean ready = canControlMagnification + ? (mServiceConnected && mMagnificationSystemUIConnected) + : mServiceConnected; + if (ready) { + getMainExecutor().execute(() -> onServiceConnected()); + mServiceConnectedNotified = true; + } + } + } + + @GuardedBy("mLock") + private void checkIsMagnificationSystemUIConnectedAlready() { + if (!android.view.accessibility.Flags + .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) { + return; + } + if (mMagnificationSystemUIConnected) { + return; + } + final IAccessibilityServiceConnection connection = + AccessibilityInteractionClient.getInstance(this).getConnection(mConnectionId); + if (connection != null) { + try { + boolean connected = connection.isMagnificationSystemUIConnected(); + mMagnificationSystemUIConnected = connected; + } catch (RemoteException re) { + Log.w(LOG_TAG, "Failed to check magnification system ui connection", re); + re.rethrowFromSystemServer(); + } + } } private void updateInputMethod(AccessibilityServiceInfo info) { @@ -1360,6 +1424,22 @@ public abstract class AccessibilityService extends Service { } } + private void onMagnificationSystemUIConnectionChanged(boolean connected) { + if (!android.view.accessibility.Flags + .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) { + return; + } + + synchronized (mLock) { + boolean changed = (mMagnificationSystemUIConnected != connected); + mMagnificationSystemUIConnected = connected; + + if (changed) { + notifyOnServiceConnectedIfReady(); + } + } + } + private void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { MagnificationController controller; @@ -2823,6 +2903,11 @@ public abstract class AccessibilityService extends Service { } @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) { + AccessibilityService.this.onMagnificationSystemUIConnectionChanged(connected); + } + + @Override public void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { AccessibilityService.this.onMagnificationChanged(displayId, region, config); @@ -3032,6 +3117,16 @@ public abstract class AccessibilityService extends Service { }); } + @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) { + mExecutor.execute(() -> { + if (mConnectionId != AccessibilityInteractionClient.NO_ID) { + mCallback.onMagnificationSystemUIConnectionChanged(connected); + } + return; + }); + } + /** Magnification changed callbacks for different displays */ public void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index 3bc61e560d8c..f1479ef79dd9 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -48,6 +48,8 @@ import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; void onKeyEvent(in KeyEvent event, int sequence); + void onMagnificationSystemUIConnectionChanged(boolean connected); + void onMagnificationChanged(int displayId, in Region region, in MagnificationConfig config); void onMotionEvent(in MotionEvent event); diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 713d8e5dd12f..149e7194a43b 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -130,6 +130,9 @@ interface IAccessibilityServiceConnection { void setMagnificationCallbackEnabled(int displayId, boolean enabled); @RequiresNoPermission + boolean isMagnificationSystemUIConnected(); + + @RequiresNoPermission boolean setSoftKeyboardShowMode(int showMode); @RequiresNoPermission diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index c0f723241c82..5956e2bde242 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -2617,6 +2617,9 @@ public class ApplicationPackageManager extends PackageManager { try { Objects.requireNonNull(packageName); return mPM.isAppArchivable(packageName, new UserHandle(getUserId())); + } catch (ParcelableException e) { + e.maybeRethrow(NameNotFoundException.class); + throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 329fb00c1d9b..fc3bb0288d67 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -114,7 +114,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ContrastColorUtil; -import com.android.internal.util.NewlineNormalizer; +import com.android.internal.util.NotificationBigTextNormalizer; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -3262,12 +3262,12 @@ public class Notification implements Parcelable return cs.toString(); } - private static CharSequence cleanUpNewLines(@Nullable CharSequence charSequence) { + private static CharSequence normalizeBigText(@Nullable CharSequence charSequence) { if (charSequence == null) { return charSequence; } - return NewlineNormalizer.normalizeNewlines(charSequence.toString()); + return NotificationBigTextNormalizer.normalizeBigText(charSequence.toString()); } private static CharSequence removeTextSizeSpans(CharSequence charSequence) { @@ -8566,7 +8566,7 @@ public class Notification implements Parcelable // Replace the text with the big text, but only if the big text is not empty. CharSequence bigTextText = mBuilder.processLegacyText(mBigText); if (Flags.cleanUpSpansAndNewLines()) { - bigTextText = cleanUpNewLines(stripStyling(bigTextText)); + bigTextText = normalizeBigText(stripStyling(bigTextText)); } if (!TextUtils.isEmpty(bigTextText)) { p.text(bigTextText); diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 348d4d8fd809..273a79efb591 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -1969,6 +1969,11 @@ public final class UiAutomation { } @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) { + /* do nothing */ + } + + @Override public void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { /* do nothing */ diff --git a/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java index ed18a057118c..eb31db18473f 100644 --- a/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java +++ b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java @@ -27,6 +27,8 @@ import android.view.IWindow; import android.view.InsetsSourceControl; import android.view.InsetsState; +import com.android.internal.annotations.VisibleForTesting; + import java.util.Objects; /** @@ -38,7 +40,9 @@ public class WindowStateInsetsControlChangeItem extends WindowStateTransactionIt private static final String TAG = "WindowStateInsetsControlChangeItem"; private InsetsState mInsetsState; - private InsetsSourceControl.Array mActiveControls; + + @VisibleForTesting + public InsetsSourceControl.Array mActiveControls; @Override public void execute(@NonNull ClientTransactionHandler client, @NonNull IWindow window, @@ -51,6 +55,8 @@ public class WindowStateInsetsControlChangeItem extends WindowStateTransactionIt // An exception could happen if the process is restarted. It is safe to ignore since // the window should no longer exist. Log.w(TAG, "The original window no longer exists in the new process", e); + // Prevent leak + mActiveControls.release(); } Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig index 374be6fd2272..18cfca686107 100644 --- a/core/java/android/appwidget/flags.aconfig +++ b/core/java/android/appwidget/flags.aconfig @@ -40,3 +40,13 @@ flag { description: "Throttle the widget view updates to mitigate transaction exceptions" bug: "326145514" } + +flag { + name: "support_resume_restore_after_reboot" + namespace: "app_widgets" + description: "Enable support for resume restore widget after reboot by persisting intermediate states to disk" + bug: "336976070" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index ed5d66227574..1e7815329f3b 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -64,3 +64,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "virtual_devices" + name: "intent_interception_action_matching_fix" + description: "Do not match intents without actions if the filter has actions" + bug: "343805037" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 270fc32a4e32..bbd0e9ff4738 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2431,6 +2431,7 @@ public class PackageInstaller { statusReceiver, new UserHandle(mUserId)); } catch (ParcelableException e) { e.maybeRethrow(PackageManager.NameNotFoundException.class); + throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2467,6 +2468,7 @@ public class PackageInstaller { } catch (ParcelableException e) { e.maybeRethrow(IOException.class); e.maybeRethrow(PackageManager.NameNotFoundException.class); + throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2499,6 +2501,7 @@ public class PackageInstaller { userActionIntent, new UserHandle(mUserId)); } catch (ParcelableException e) { e.maybeRethrow(PackageManager.NameNotFoundException.class); + throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index c674968bba8a..0dec13ff0c02 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -18,9 +18,10 @@ package android.text; import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; -import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; +import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; +import android.annotation.ColorInt; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; @@ -398,6 +399,20 @@ public abstract class Layout { mUseBoundsForWidth = useBoundsForWidth; mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang; mMinimumFontMetrics = minimumFontMetrics; + + initSpanColors(); + } + + private void initSpanColors() { + if (mSpannedText && Flags.highContrastTextSmallTextRect()) { + if (mSpanColors == null) { + mSpanColors = new SpanColors(); + } else { + mSpanColors.recycle(); + } + } else { + mSpanColors = null; + } } /** @@ -417,6 +432,7 @@ public abstract class Layout { mSpacingMult = spacingmult; mSpacingAdd = spacingadd; mSpannedText = text instanceof Spanned; + initSpanColors(); } /** @@ -643,20 +659,20 @@ public abstract class Layout { return null; } - return isHighContrastTextDark() ? BlendMode.MULTIPLY : BlendMode.DIFFERENCE; + return isHighContrastTextDark(mPaint.getColor()) ? BlendMode.MULTIPLY + : BlendMode.DIFFERENCE; } - private boolean isHighContrastTextDark() { + private boolean isHighContrastTextDark(@ColorInt int color) { // High-contrast text mode // Determine if the text is black-on-white or white-on-black, so we know what blendmode will // give the highest contrast and most realistic text color. // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h if (highContrastTextLuminance()) { var lab = new double[3]; - ColorUtils.colorToLAB(mPaint.getColor(), lab); - return lab[0] < 0.5; + ColorUtils.colorToLAB(color, lab); + return lab[0] < 50.0; } else { - var color = mPaint.getColor(); int channelSum = Color.red(color) + Color.green(color) + Color.blue(color); return channelSum < (128 * 3); } @@ -1010,15 +1026,22 @@ public abstract class Layout { var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX, mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR); + var originalTextColor = mPaint.getColor(); var bgPaint = mWorkPlainPaint; bgPaint.reset(); - bgPaint.setColor(isHighContrastTextDark() ? Color.WHITE : Color.BLACK); + bgPaint.setColor(isHighContrastTextDark(originalTextColor) ? Color.WHITE : Color.BLACK); bgPaint.setStyle(Paint.Style.FILL); int start = getLineStart(firstLine); int end = getLineEnd(lastLine); // Draw a separate background rectangle for each line of text, that only surrounds the - // characters on that line. + // characters on that line. But we also have to check the text color for each character, and + // make sure we are drawing the correct contrasting background. This is because Spans can + // change colors throughout the text and we'll need to match our backgrounds. + if (mSpannedText && mSpanColors != null) { + mSpanColors.init(mWorkPaint, ((Spanned) mText), start, end); + } + forEachCharacterBounds( start, end, @@ -1028,13 +1051,24 @@ public abstract class Layout { int mLastLineNum = -1; final RectF mLineBackground = new RectF(); + @ColorInt int mLastColor = originalTextColor; + @Override public void onCharacterBounds(int index, int lineNum, float left, float top, float right, float bottom) { - if (lineNum != mLastLineNum) { + + var newBackground = determineContrastingBackgroundColor(index); + var hasBgColorChanged = newBackground != bgPaint.getColor(); + + if (lineNum != mLastLineNum || hasBgColorChanged) { + // Draw what we have so far, then reset the rect and update its color drawRect(); mLineBackground.set(left, top, right, bottom); mLastLineNum = lineNum; + + if (hasBgColorChanged) { + bgPaint.setColor(newBackground); + } } else { mLineBackground.union(left, top, right, bottom); } @@ -1051,8 +1085,36 @@ public abstract class Layout { canvas.drawRect(mLineBackground, bgPaint); } } + + private int determineContrastingBackgroundColor(int index) { + if (!mSpannedText || mSpanColors == null) { + // The text is not Spanned. it's all one color. + return bgPaint.getColor(); + } + + // Sometimes the color will change, but not enough to warrant a background + // color change. e.g. from black to dark grey still gets clamped to black, + // so the background stays white and we don't need to draw a fresh + // background. + var textColor = mSpanColors.getColorAt(index); + if (textColor == SpanColors.NO_COLOR_FOUND) { + textColor = originalTextColor; + } + var hasColorChanged = textColor != mLastColor; + if (hasColorChanged) { + mLastColor = textColor; + + return isHighContrastTextDark(textColor) ? Color.WHITE : Color.BLACK; + } + + return bgPaint.getColor(); + } } ); + + if (mSpanColors != null) { + mSpanColors.recycle(); + } } /** @@ -3580,6 +3642,7 @@ public abstract class Layout { private float mSpacingAdd; private static final Rect sTempRect = new Rect(); private boolean mSpannedText; + @Nullable private SpanColors mSpanColors; private TextDirectionHeuristic mTextDir; private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; private boolean mIncludePad; diff --git a/core/java/android/text/SpanColors.java b/core/java/android/text/SpanColors.java new file mode 100644 index 000000000000..fcd242b62700 --- /dev/null +++ b/core/java/android/text/SpanColors.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.annotation.ColorInt; +import android.annotation.Nullable; +import android.graphics.Color; +import android.text.style.CharacterStyle; + +/** + * Finds the foreground text color for the given Spanned text so you can iterate through each color + * change. + * + * @hide + */ +public class SpanColors { + public static final @ColorInt int NO_COLOR_FOUND = Color.TRANSPARENT; + + private final SpanSet<CharacterStyle> mCharacterStyleSpanSet = + new SpanSet<>(CharacterStyle.class); + @Nullable private TextPaint mWorkPaint; + + public SpanColors() {} + + /** + * Init for the given text + * + * @param workPaint A temporary TextPaint object that will be used to calculate the colors. The + * paint properties will be mutated on calls to {@link #getColorAt(int)} so + * make sure to reset it before you use it for something else. + * @param spanned the text to examine + * @param start index to start at + * @param end index of the end + */ + public void init(TextPaint workPaint, Spanned spanned, int start, int end) { + mWorkPaint = workPaint; + mCharacterStyleSpanSet.init(spanned, start, end); + } + + /** + * Removes all internal references to the spans to avoid memory leaks. + */ + public void recycle() { + mWorkPaint = null; + mCharacterStyleSpanSet.recycle(); + } + + /** + * Calculates the foreground color of the text at the given character index. + * + * <p>You must call {@link #init(TextPaint, Spanned, int, int)} before calling this + */ + public @ColorInt int getColorAt(int index) { + var finalColor = NO_COLOR_FOUND; + // Reset the paint so if we get a CharacterStyle that doesn't actually specify color, + // (like UnderlineSpan), we still return no color found. + mWorkPaint.setColor(finalColor); + for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { + if ((index >= mCharacterStyleSpanSet.spanStarts[k]) + && (index <= mCharacterStyleSpanSet.spanEnds[k])) { + final CharacterStyle span = mCharacterStyleSpanSet.spans[k]; + span.updateDrawState(mWorkPaint); + + finalColor = calculateFinalColor(mWorkPaint); + } + } + return finalColor; + } + + private @ColorInt int calculateFinalColor(TextPaint workPaint) { + // TODO: can we figure out what the getColorFilter() will do? + // if so, we also need to reset colorFilter before the loop in getColorAt() + return workPaint.getColor(); + } +} diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java index 588e9e0a3b12..487214c5c33a 100644 --- a/core/java/android/view/InsetsSourceControl.java +++ b/core/java/android/view/InsetsSourceControl.java @@ -305,6 +305,18 @@ public class InsetsSourceControl implements Parcelable { return mControls; } + /** Cleanup {@link SurfaceControl} stored in controls to prevent leak. */ + public void release() { + if (mControls == null) { + return; + } + for (InsetsSourceControl control : mControls) { + if (control != null) { + control.release(SurfaceControl::release); + } + } + } + /** Sets the given flags to all controls. */ public void setParcelableFlags(int parcelableFlags) { if (mControls == null) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b54e052cf538..496e8992fc41 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2294,12 +2294,8 @@ public final class ViewRootImpl implements ViewParent, mInsetsController.onStateChanged(insetsState); if (mAdded) { mInsetsController.onControlsChanged(controls); - } else if (controls != null) { - for (InsetsSourceControl control : controls) { - if (control != null) { - control.release(SurfaceControl::release); - } - } + } else { + activeControls.release(); } } @@ -11306,6 +11302,9 @@ public final class ViewRootImpl implements ViewParent, mIsFromTransactionItem = false; final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor == null) { + if (isFromInsetsControlChangeItem) { + activeControls.release(); + } return; } if (insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) { diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java index 12e08148a651..1fe8180aa7b2 100644 --- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java +++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java @@ -302,6 +302,10 @@ public abstract class AccessibilityDisplayProxy { } @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) { + } + + @Override public void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { } diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index ab7b2261dc17..edf33875b765 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -169,3 +169,13 @@ flag { description: "Feature flag for declaring system pinch zoom opt-out apis" bug: "315089687" } + +flag { + name: "wait_magnification_system_ui_connection_to_notify_service_connected" + namespace: "accessibility" + description: "Decide whether AccessibilityService needs to wait until magnification system ui connection is ready to trigger onServiceConnected" + bug: "337800504" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java index 1bd921b339f6..d5398e6268dc 100644 --- a/core/java/android/window/ClientWindowFrames.java +++ b/core/java/android/window/ClientWindowFrames.java @@ -56,7 +56,16 @@ public class ClientWindowFrames implements Parcelable { public ClientWindowFrames() { } - public ClientWindowFrames(ClientWindowFrames other) { + public ClientWindowFrames(@NonNull ClientWindowFrames other) { + setTo(other); + } + + private ClientWindowFrames(@NonNull Parcel in) { + readFromParcel(in); + } + + /** Updates the current frames to the given frames. */ + public void setTo(@NonNull ClientWindowFrames other) { frame.set(other.frame); displayFrame.set(other.displayFrame); parentFrame.set(other.parentFrame); @@ -67,10 +76,6 @@ public class ClientWindowFrames implements Parcelable { compatScale = other.compatScale; } - private ClientWindowFrames(Parcel in) { - readFromParcel(in); - } - /** Needed for AIDL out parameters. */ public void readFromParcel(Parcel in) { frame.readFromParcel(in); diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java index f928f509bdb6..4c8bad6d0aff 100644 --- a/core/java/android/window/SnapshotDrawerUtils.java +++ b/core/java/android/window/SnapshotDrawerUtils.java @@ -77,6 +77,14 @@ public class SnapshotDrawerUtils { private static final String TAG = "SnapshotDrawerUtils"; /** + * Used to check if toolkitSetFrameRateReadOnly flag is enabled + * + * @hide + */ + private static boolean sToolkitSetFrameRateReadOnlyFlagValue = + android.view.flags.Flags.toolkitSetFrameRateReadOnly(); + + /** * When creating the starting window, we use the exact same layout flags such that we end up * with a window with the exact same dimensions etc. However, these flags are not used in layout * and might cause other side effects so we exclude them. @@ -439,6 +447,9 @@ public class SnapshotDrawerUtils { layoutParams.setFitInsetsTypes(attrs.getFitInsetsTypes()); layoutParams.setFitInsetsSides(attrs.getFitInsetsSides()); layoutParams.setFitInsetsIgnoringVisibility(attrs.isFitInsetsIgnoringVisibility()); + if (sToolkitSetFrameRateReadOnlyFlagValue) { + layoutParams.setFrameRatePowerSavingsBalanced(false); + } layoutParams.setTitle(title); layoutParams.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL; diff --git a/core/java/com/android/internal/util/NewlineNormalizer.java b/core/java/com/android/internal/util/NewlineNormalizer.java deleted file mode 100644 index 0104d1f56f83..000000000000 --- a/core/java/com/android/internal/util/NewlineNormalizer.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.util; - - -import java.util.regex.Pattern; - -/** - * Utility class that replaces consecutive empty lines with single new line. - * @hide - */ -public class NewlineNormalizer { - - private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?"); - - // Private constructor to prevent instantiation - private NewlineNormalizer() {} - - /** - * Replaces consecutive newlines with a single newline in the input text. - */ - public static String normalizeNewlines(String text) { - return MULTIPLE_NEWLINES.matcher(text).replaceAll("\n"); - } -} diff --git a/core/java/com/android/internal/util/NotificationBigTextNormalizer.java b/core/java/com/android/internal/util/NotificationBigTextNormalizer.java new file mode 100644 index 000000000000..80d409500ef0 --- /dev/null +++ b/core/java/com/android/internal/util/NotificationBigTextNormalizer.java @@ -0,0 +1,123 @@ +/* + * 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.internal.util; + + +import android.annotation.NonNull; +import android.os.Trace; + +import java.util.regex.Pattern; + +/** + * Utility class that normalizes BigText style Notification content. + * @hide + */ +public class NotificationBigTextNormalizer { + + private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?"); + private static final Pattern HORIZONTAL_WHITESPACES = Pattern.compile("\\h+"); + + // Private constructor to prevent instantiation + private NotificationBigTextNormalizer() {} + + /** + * Normalizes the given text by collapsing consecutive new lines into single one and cleaning + * up each line by removing zero-width characters, invisible formatting characters, and + * collapsing consecutive whitespace into single space. + */ + @NonNull + public static String normalizeBigText(@NonNull String text) { + try { + Trace.beginSection("NotifBigTextNormalizer#normalizeBigText"); + text = MULTIPLE_NEWLINES.matcher(text).replaceAll("\n"); + text = HORIZONTAL_WHITESPACES.matcher(text).replaceAll(" "); + text = normalizeLines(text); + return text; + } finally { + Trace.endSection(); + } + } + + /** + * Normalizes lines in a text by removing zero-width characters, invisible formatting + * characters, and collapsing consecutive whitespace into single space. + * + * <p> + * This method processes the input text line by line. It eliminates zero-width + * characters (U+200B to U+200D, U+FEFF, U+034F), invisible formatting + * characters (U+2060 to U+2065, U+206A to U+206F, U+FFF9 to U+FFFB), + * and replaces any sequence of consecutive whitespace characters with a single space. + * </p> + * + * <p> + * Additionally, the method trims trailing whitespace from each line and removes any + * resulting empty lines. + * </p> + */ + @NonNull + private static String normalizeLines(@NonNull String text) { + String[] lines = text.split("\n"); + final StringBuilder textSB = new StringBuilder(text.length()); + for (int i = 0; i < lines.length; i++) { + final String line = lines[i]; + final StringBuilder lineSB = new StringBuilder(line.length()); + boolean spaceSeen = false; + for (int j = 0; j < line.length(); j++) { + final char character = line.charAt(j); + + // Skip ZERO WIDTH characters + if ((character >= '\u200B' && character <= '\u200D') + || character == '\uFEFF' || character == '\u034F') { + continue; + } + // Skip INVISIBLE_FORMATTING_CHARACTERS + if ((character >= '\u2060' && character <= '\u2065') + || (character >= '\u206A' && character <= '\u206F') + || (character >= '\uFFF9' && character <= '\uFFFB')) { + continue; + } + + if (isSpace(character)) { + // eliminate consecutive spaces.... + if (!spaceSeen) { + lineSB.append(" "); + } + spaceSeen = true; + } else { + spaceSeen = false; + lineSB.append(character); + } + } + // trim line. + final String currentLine = lineSB.toString().trim(); + + // don't add empty lines after trim. + if (currentLine.length() > 0) { + if (textSB.length() > 0) { + textSB.append("\n"); + } + textSB.append(currentLine); + } + } + + return textSB.toString(); + } + + private static boolean isSpace(char ch) { + return ch != '\n' && Character.isSpaceChar(ch); + } +} diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index d48cdc4645c6..eaff7608ce3b 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -713,6 +713,19 @@ android_media_AudioSystem_getForceUse(JNIEnv *env, jobject thiz, jint usage) AudioSystem::getForceUse(static_cast<audio_policy_force_use_t>(usage))); } +static jint android_media_AudioSystem_setDeviceAbsoluteVolumeEnabled(JNIEnv *env, jobject thiz, + jint device, jstring address, + jboolean enabled, + jint stream) { + const char *c_address = env->GetStringUTFChars(address, nullptr); + int state = check_AudioSystem_Command( + AudioSystem::setDeviceAbsoluteVolumeEnabled(static_cast<audio_devices_t>(device), + c_address, enabled, + static_cast<audio_stream_type_t>(stream))); + env->ReleaseStringUTFChars(address, c_address); + return state; +} + static jint android_media_AudioSystem_initStreamVolume(JNIEnv *env, jobject thiz, jint stream, jint indexMin, jint indexMax) { @@ -3373,6 +3386,7 @@ static const JNINativeMethod gMethods[] = MAKE_AUDIO_SYSTEM_METHOD(setPhoneState), MAKE_AUDIO_SYSTEM_METHOD(setForceUse), MAKE_AUDIO_SYSTEM_METHOD(getForceUse), + MAKE_AUDIO_SYSTEM_METHOD(setDeviceAbsoluteVolumeEnabled), MAKE_AUDIO_SYSTEM_METHOD(initStreamVolume), MAKE_AUDIO_SYSTEM_METHOD(setStreamVolumeIndex), MAKE_AUDIO_SYSTEM_METHOD(getStreamVolumeIndex), diff --git a/core/proto/android/app/appexitinfo.proto b/core/proto/android/app/appexitinfo.proto index 3abc462671a2..e560a944b94b 100644 --- a/core/proto/android/app/appexitinfo.proto +++ b/core/proto/android/app/appexitinfo.proto @@ -20,7 +20,7 @@ option java_multiple_files = true; package android.app; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/app_enums.proto"; /** * An android.app.ApplicationExitInfo object. diff --git a/core/proto/android/app/appstartinfo.proto b/core/proto/android/app/appstartinfo.proto index d9ed911515ba..c13753343ba8 100644 --- a/core/proto/android/app/appstartinfo.proto +++ b/core/proto/android/app/appstartinfo.proto @@ -20,7 +20,7 @@ option java_multiple_files = true; package android.app; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/app_enums.proto"; /** * An android.app.ApplicationStartInfo object. diff --git a/core/proto/android/os/batterystats.proto b/core/proto/android/os/batterystats.proto index 4c84944a7382..97f81484b84d 100644 --- a/core/proto/android/os/batterystats.proto +++ b/core/proto/android/os/batterystats.proto @@ -21,7 +21,7 @@ package android.os; import "frameworks/base/core/proto/android/os/powermanager.proto"; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/job/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/job/job_enums.proto"; import "frameworks/proto_logging/stats/enums/telephony/enums.proto"; message BatteryStatsProto { diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index e3a438da5abc..921c41c8e52b 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -35,7 +35,7 @@ import "frameworks/base/core/proto/android/server/intentresolver.proto"; import "frameworks/base/core/proto/android/server/windowmanagerservice.proto"; import "frameworks/base/core/proto/android/util/common.proto"; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/app_enums.proto"; option java_multiple_files = true; diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 00127c134ce6..a1e3dc1b9b4e 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -31,7 +31,7 @@ import "frameworks/base/core/proto/android/server/appstatetracker.proto"; import "frameworks/base/core/proto/android/server/statlogger.proto"; import "frameworks/base/core/proto/android/privacy.proto"; import "frameworks/base/core/proto/android/util/quotatracker.proto"; -import "frameworks/proto_logging/stats/enums/app/job/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/job/job_enums.proto"; import "frameworks/proto_logging/stats/enums/server/job/enums.proto"; message JobSchedulerServiceDumpProto { diff --git a/core/proto/android/server/powermanagerservice.proto b/core/proto/android/server/powermanagerservice.proto index 2f865afd28c7..593bbc6f5d0d 100644 --- a/core/proto/android/server/powermanagerservice.proto +++ b/core/proto/android/server/powermanagerservice.proto @@ -26,7 +26,7 @@ import "frameworks/base/core/proto/android/os/worksource.proto"; import "frameworks/base/core/proto/android/providers/settings.proto"; import "frameworks/base/core/proto/android/server/wirelesschargerdetector.proto"; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/app_enums.proto"; import "frameworks/proto_logging/stats/enums/os/enums.proto"; import "frameworks/proto_logging/stats/enums/view/enums.proto"; diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java index 3735274c1a6c..c7060adc1ca1 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java @@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.Activity; @@ -39,7 +41,6 @@ import android.view.InsetsSourceControl; import android.view.InsetsState; import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; -import android.window.WindowContext; import android.window.WindowContextInfo; import androidx.test.filters.SmallTest; @@ -73,8 +74,6 @@ public class ClientTransactionItemTest { @Mock private IBinder mWindowClientToken; @Mock - private WindowContext mWindowContext; - @Mock private IWindow mWindow; // Can't mock final class. @@ -176,4 +175,17 @@ public class ClientTransactionItemTest { verify(mWindow).insetsControlChanged(mInsetsState, mActiveControls); } + + @Test + public void testWindowStateInsetsControlChangeItem_executeError() throws RemoteException { + doThrow(new RemoteException()).when(mWindow).insetsControlChanged(any(), any()); + + mActiveControls = spy(mActiveControls); + final WindowStateInsetsControlChangeItem item = WindowStateInsetsControlChangeItem.obtain( + mWindow, mInsetsState, mActiveControls); + item.mActiveControls = mActiveControls; + item.execute(mHandler, mPendingActions); + + verify(mActiveControls).release(); + } } diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java index 1c1236279b61..98f8b7fc897c 100644 --- a/core/tests/coretests/src/android/text/LayoutTest.java +++ b/core/tests/coretests/src/android/text/LayoutTest.java @@ -39,6 +39,7 @@ import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.text.Layout.Alignment; +import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import androidx.test.filters.SmallTest; @@ -933,6 +934,83 @@ public class LayoutTest { expect.that(numBackgroundsFound).isEqualTo(backgroundRectsDrawn); } + @Test + @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void highContrastTextEnabled_testDrawMulticolorText_drawsBlackAndWhiteBackgrounds() { + /* + Here's what the final render should look like: + + Text | Background + ======================== + al | BW + w | WW + ei | WW + \t; | WW + s | BB + df | BB + s | BB + df | BB + @ | BB + ------------------------ + */ + + mTextPaint.setColor(Color.WHITE); + + mSpannedText.setSpan( + // Can't use DKGREY because it is right on the cusp of clamping white + new ForegroundColorSpan(0xFF332211), + /* start= */ 1, + /* end= */ 6, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ); + mSpannedText.setSpan( + new ForegroundColorSpan(Color.LTGRAY), + /* start= */ 8, + /* end= */ 11, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ); + Layout layout = new StaticLayout(mSpannedText, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); + + final int width = 256; + final int height = 256; + MockCanvas c = new MockCanvas(width, height); + c.setHighContrastTextEnabled(true); + layout.draw( + c, + /* highlightPaths= */ null, + /* highlightPaints= */ null, + /* selectionPath= */ null, + /* selectionPaint= */ null, + /* cursorOffsetVertical= */ 0 + ); + List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); + var highlightsDrawn = 0; + var numColorChangesWithinOneLine = 1; + var textsDrawn = STATIC_LINE_COUNT + numColorChangesWithinOneLine; + var backgroundRectsDrawn = STATIC_LINE_COUNT + numColorChangesWithinOneLine; + expect.withMessage("wrong number of drawCommands: " + drawCommands) + .that(drawCommands.size()) + .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn); + + var backgroundCommands = drawCommands.stream() + .filter(it -> it.rect != null) + .toList(); + + expect.that(backgroundCommands.get(0).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(1).paint.getColor()).isEqualTo(Color.WHITE); + expect.that(backgroundCommands.get(2).paint.getColor()).isEqualTo(Color.WHITE); + expect.that(backgroundCommands.get(3).paint.getColor()).isEqualTo(Color.WHITE); + expect.that(backgroundCommands.get(4).paint.getColor()).isEqualTo(Color.WHITE); + expect.that(backgroundCommands.get(5).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(6).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(7).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(8).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(9).paint.getColor()).isEqualTo(Color.BLACK); + + expect.that(backgroundCommands.size()).isEqualTo(backgroundRectsDrawn); + } + private static final class MockCanvas extends Canvas { static class DrawCommand { diff --git a/core/tests/coretests/src/android/text/SpanColorsTest.java b/core/tests/coretests/src/android/text/SpanColorsTest.java new file mode 100644 index 000000000000..3d8d8f9c126d --- /dev/null +++ b/core/tests/coretests/src/android/text/SpanColorsTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import android.graphics.drawable.ShapeDrawable; +import android.platform.test.annotations.Presubmit; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.UnderlineSpan; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SpanColorsTest { + private final TextPaint mWorkPaint = new TextPaint(); + private SpanColors mSpanColors; + private SpannableString mSpannedText; + + @Before + public void setup() { + mSpanColors = new SpanColors(); + mSpannedText = new SpannableString("Hello world! This is a test."); + mSpannedText.setSpan(new ForegroundColorSpan(Color.RED), 0, 4, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + mSpannedText.setSpan(new ForegroundColorSpan(Color.GREEN), 6, 11, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + mSpannedText.setSpan(new UnderlineSpan(), 5, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + mSpannedText.setSpan(new ImageSpan(new ShapeDrawable()), 1, 2, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + mSpannedText.setSpan(new ForegroundColorSpan(Color.BLUE), 12, 16, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void testNoColorFound() { + mSpanColors.init(mWorkPaint, mSpannedText, 25, 30); // Beyond the spans + assertThat(mSpanColors.getColorAt(27)).isEqualTo(SpanColors.NO_COLOR_FOUND); + } + + @Test + public void testSingleColorSpan() { + mSpanColors.init(mWorkPaint, mSpannedText, 1, 4); + assertThat(mSpanColors.getColorAt(3)).isEqualTo(Color.RED); + } + + @Test + public void testMultipleColorSpans() { + mSpanColors.init(mWorkPaint, mSpannedText, 0, mSpannedText.length()); + assertThat(mSpanColors.getColorAt(2)).isEqualTo(Color.RED); + assertThat(mSpanColors.getColorAt(5)).isEqualTo(SpanColors.NO_COLOR_FOUND); + assertThat(mSpanColors.getColorAt(8)).isEqualTo(Color.GREEN); + assertThat(mSpanColors.getColorAt(13)).isEqualTo(Color.BLUE); + } +} diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java index b5c264c4ae5e..5a6824bf0d7e 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java @@ -146,6 +146,10 @@ public class AccessibilityServiceConnectionImpl extends IAccessibilityServiceCon public void setMagnificationCallbackEnabled(int displayId, boolean enabled) {} + public boolean isMagnificationSystemUIConnected() { + return false; + } + public boolean setSoftKeyboardShowMode(int showMode) { return false; } diff --git a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java deleted file mode 100644 index bcdac610a49d..000000000000 --- a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.util; - -import static junit.framework.Assert.assertEquals; - - -import android.platform.test.annotations.DisabledOnRavenwood; -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test for {@link NewlineNormalizer} - * @hide - */ -@DisabledOnRavenwood(blockedBy = NewlineNormalizer.class) -@RunWith(AndroidJUnit4.class) -public class NewlineNormalizerTest { - - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule(); - - @Test - public void testEmptyInput() { - assertEquals("", NewlineNormalizer.normalizeNewlines("")); - } - - @Test - public void testSingleNewline() { - assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n")); - } - - @Test - public void testMultipleConsecutiveNewlines() { - assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n\n\n\n\n")); - } - - @Test - public void testNewlinesWithSpacesAndTabs() { - String input = "Line 1\n \n \t \n\tLine 2"; - // Adjusted expected output to include the tab character - String expected = "Line 1\n\tLine 2"; - assertEquals(expected, NewlineNormalizer.normalizeNewlines(input)); - } - - @Test - public void testMixedNewlineCharacters() { - String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6"; - String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6"; - assertEquals(expected, NewlineNormalizer.normalizeNewlines(input)); - } -} diff --git a/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java new file mode 100644 index 000000000000..1f2e24aa8c68 --- /dev/null +++ b/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java @@ -0,0 +1,148 @@ +/* + * 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.internal.util; + +import static junit.framework.Assert.assertEquals; + + +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test for {@link NotificationBigTextNormalizer} + * @hide + */ +@DisabledOnRavenwood(blockedBy = NotificationBigTextNormalizer.class) +@RunWith(AndroidJUnit4.class) +public class NotificationBigTextNormalizerTest { + + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + + @Test + public void testEmptyInput() { + assertEquals("", NotificationBigTextNormalizer.normalizeBigText("")); + } + + @Test + public void testSingleNewline() { + assertEquals("", NotificationBigTextNormalizer.normalizeBigText("\n")); + } + + @Test + public void testMultipleConsecutiveNewlines() { + assertEquals("", NotificationBigTextNormalizer.normalizeBigText("\n\n\n\n\n")); + } + + @Test + public void testNewlinesWithSpacesAndTabs() { + String input = "Line 1\n \n \t \n\tLine 2"; + // Adjusted expected output to include the tab character + String expected = "Line 1\nLine 2"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + + @Test + public void testMixedNewlineCharacters() { + String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6"; + String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + + @Test + public void testConsecutiveSpaces() { + // Only spaces + assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This" + + " is a test.")); + // Zero width characters bw spaces. + assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This" + + "\u200B \u200B \u200B \u200B \u200B \u200B \u200B \u200Bis\uFEFF \uFEFF \uFEFF" + + " \uFEFFa \u034F \u034F \u034F \u034F \u034F \u034Ftest.")); + + // Invisible formatting characters bw spaces. + assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This" + + "\u2061 \u2061 \u2061 \u2061 \u2061 \u2061 \u2061 \u2061is\u206E \u206E \u206E" + + " \u206Ea \uFFFB \uFFFB \uFFFB \uFFFB \uFFFB \uFFFBtest.")); + // Non breakable spaces + assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This" + + "\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0is\u2005 \u2005 \u2005" + + " \u2005a\u2005\u2005\u2005 \u2005\u2005\u2005test.")); + } + + @Test + public void testZeroWidthCharRemoval() { + // Test each character individually + char[] zeroWidthChars = { '\u200B', '\u200C', '\u200D', '\uFEFF', '\u034F' }; + + for (char c : zeroWidthChars) { + String input = "Test" + c + "string"; + String expected = "Teststring"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + } + + @Test + public void testWhitespaceReplacement() { + assertEquals("This text has horizontal whitespace.", + NotificationBigTextNormalizer.normalizeBigText( + "This\ttext\thas\thorizontal\twhitespace.")); + assertEquals("This text has mixed whitespace.", + NotificationBigTextNormalizer.normalizeBigText( + "This text has \u00A0 mixed\u2009whitespace.")); + assertEquals("This text has leading and trailing whitespace.", + NotificationBigTextNormalizer.normalizeBigText( + "\t This text has leading and trailing whitespace. \n")); + } + + @Test + public void testInvisibleFormattingCharacterRemoval() { + // Test each character individually + char[] invisibleFormattingChars = { + '\u2060', '\u2061', '\u2062', '\u2063', '\u2064', '\u2065', + '\u206A', '\u206B', '\u206C', '\u206D', '\u206E', '\u206F', + '\uFFF9', '\uFFFA', '\uFFFB' + }; + + for (char c : invisibleFormattingChars) { + String input = "Test " + c + "string"; + String expected = "Test string"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + } + @Test + public void testNonBreakSpaceReplacement() { + // Test each character individually + char[] nonBreakSpaces = { + '\u00A0', '\u1680', '\u2000', '\u2001', '\u2002', + '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', + '\u2008', '\u2009', '\u200A', '\u202F', '\u205F', '\u3000' + }; + + for (char c : nonBreakSpaces) { + String input = "Test" + c + "string"; + String expected = "Test string"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + } +} diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java index d359a9050a0f..3cff91597939 100644 --- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java +++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java @@ -1149,6 +1149,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu /** * Sets the serial number used for the certificate of the generated key pair. + * To ensure compatibility with devices and certificate parsers, the value + * should be 20 bytes or shorter (see RFC 5280 section 4.1.2.2). * * <p>By default, the serial number is {@code 1}. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 6224543516fa..6ade81c0f3a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -1591,7 +1591,7 @@ public class Transitions implements RemoteCallable<Transitions>, public void setHomeTransitionListener(IHomeTransitionListener listener) { executeRemoteCallWithTaskPermission(mTransitions, "setHomeTransitionListener", (transitions) -> { - transitions.mHomeTransitionObserver.setHomeTransitionListener(mTransitions, + transitions.mHomeTransitionObserver.setHomeTransitionListener(transitions, listener); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 6901e75019bc..37cdbb47bfe8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -583,17 +583,20 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (ev.getAction() == ACTION_HOVER_MOVE && MaximizeMenu.Companion.isMaximizeMenuView(id)) { decoration.onMaximizeMenuHoverMove(id, ev); + mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); } else if (ev.getAction() == ACTION_HOVER_EXIT) { if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { decoration.onMaximizeWindowHoverExit(); - } else if (id == R.id.maximize_window || id == R.id.maximize_menu) { + } else if (id == R.id.maximize_window + || MaximizeMenu.Companion.isMaximizeMenuView(id)) { // Close menu if not hovering over maximize menu or maximize button after a // delay to give user a chance to re-enter view or to move from one maximize // menu view to another. mMainHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS); - } else if (MaximizeMenu.Companion.isMaximizeMenuView(id)) { - decoration.onMaximizeMenuHoverExit(id, ev); + if (id != R.id.maximize_window) { + decoration.onMaximizeMenuHoverExit(id, ev); + } } return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt index 78f0ef7af45c..4f049015af99 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt @@ -88,7 +88,7 @@ class MaximizeButtonView( } fun cancelHoverAnimation() { - hoverProgressAnimatorSet.removeAllListeners() + hoverProgressAnimatorSet.childAnimations.forEach { it.removeAllListeners() } hoverProgressAnimatorSet.cancel() progressBar.visibility = View.INVISIBLE } diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 293c561f166c..d148afd3c15d 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1764,6 +1764,10 @@ public class AudioSystem public static native int getForceUse(int usage); /** @hide */ @UnsupportedAppUsage + public static native int setDeviceAbsoluteVolumeEnabled(int nativeDeviceType, + @NonNull String address, boolean enabled, int streamToDriveAbs); + /** @hide */ + @UnsupportedAppUsage public static native int initStreamVolume(int stream, int indexMin, int indexMax); @UnsupportedAppUsage private static native int setStreamVolumeIndex(int stream, int index, int device); diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index a33e2252019b..055ccbced58a 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -27,6 +27,7 @@ package android.nfc { field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC"; field @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.SHOW_CUSTOMIZED_RESOLVER) public static final String ACTION_SHOW_NFC_RESOLVER = "android.nfc.action.SHOW_NFC_RESOLVER"; field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String EXTRA_RESOLVE_INFOS = "android.nfc.extra.RESOLVE_INFOS"; + field @FlaggedApi("android.nfc.nfc_set_default_disc_tech") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static final int FLAG_SET_DEFAULT_TECH = 1073741824; // 0x40000000 field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int MESSAGE_TYPE_COMMAND = 1; // 0x1 field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_FAILED = 3; // 0x3 field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_MESSAGE_CORRUPTED = 2; // 0x2 diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 698df28129be..1dfc81e2108e 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -340,7 +340,8 @@ public final class NfcAdapter { public static final int FLAG_READER_NFC_BARCODE = 0x10; /** @hide */ - @IntDef(flag = true, prefix = {"FLAG_READER_"}, value = { + @IntDef(flag = true, value = { + FLAG_SET_DEFAULT_TECH, FLAG_READER_KEEP, FLAG_READER_DISABLE, FLAG_READER_NFC_A, @@ -438,7 +439,8 @@ public final class NfcAdapter { public static final int FLAG_USE_ALL_TECH = 0xff; /** @hide */ - @IntDef(flag = true, prefix = {"FLAG_LISTEN_"}, value = { + @IntDef(flag = true, value = { + FLAG_SET_DEFAULT_TECH, FLAG_LISTEN_KEEP, FLAG_LISTEN_DISABLE, FLAG_LISTEN_NFC_PASSIVE_A, @@ -449,6 +451,18 @@ public final class NfcAdapter { public @interface ListenTechnology {} /** + * Flag used in {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag changes the default listen or poll tech. + * Only available to privileged apps. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_NFC_SET_DEFAULT_DISC_TECH) + @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) + public static final int FLAG_SET_DEFAULT_TECH = 0x40000000; + + /** * @hide * @removed */ @@ -1874,14 +1888,6 @@ public final class NfcAdapter { public void setDiscoveryTechnology(@NonNull Activity activity, @PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) { - // A special treatment of the _KEEP flags - if ((listenTechnology & FLAG_LISTEN_KEEP) != 0) { - listenTechnology = -1; - } - if ((pollTechnology & FLAG_READER_KEEP) != 0) { - pollTechnology = -1; - } - if (listenTechnology == FLAG_LISTEN_DISABLE) { synchronized (sLock) { if (!sHasNfcFeature) { @@ -1901,7 +1907,25 @@ public final class NfcAdapter { } } } - mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology); + /* + * Privileged FLAG to set technology mask for all data processed by NFC controller + * Note: Use with caution! The app is responsible for ensuring that the discovery + * technology mask is returned to default. + * Note: FLAG_USE_ALL_TECH used with _KEEP flags will reset the technolody to android default + */ + if (Flags.nfcSetDefaultDiscTech() + && ((pollTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH + || (listenTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH)) { + Binder token = new Binder(); + try { + NfcAdapter.sService.updateDiscoveryTechnology(token, + pollTechnology, listenTechnology); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } else { + mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology); + } } /** diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index cb2a48c2913f..b242a76ffae4 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -101,3 +101,12 @@ flag { description: "Enable nfc state change API" bug: "319934052" } + +flag { + name: "nfc_set_default_disc_tech" + is_exported: true + namespace: "nfc" + description: "Flag for NFC set default disc tech API" + bug: "321311407" +} + diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt index b43b5f318cf1..373b3e8b068d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt @@ -38,7 +38,6 @@ import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.creation.CreateOptionInfo import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.model.get.ProviderInfo -import java.lang.Exception /** * Aggregates common display information used for the Biometric Flow. @@ -121,11 +120,11 @@ fun runBiometricFlowForGet( getBiometricCancellationSignal: () -> CancellationSignal, getRequestDisplayInfo: RequestDisplayInfo? = null, getProviderInfoList: List<ProviderInfo>? = null, - getProviderDisplayInfo: ProviderDisplayInfo? = null, -) { + getProviderDisplayInfo: ProviderDisplayInfo? = null +): Boolean { if (getBiometricPromptState() != BiometricPromptState.INACTIVE) { // Screen is already up, do not re-launch - return + return false } onBiometricPromptStateChange(BiometricPromptState.PENDING) val biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo( @@ -137,7 +136,7 @@ fun runBiometricFlowForGet( if (biometricDisplayInfo == null) { onBiometricFailureFallback(BiometricFlowType.GET) - return + return false } val callback: BiometricPrompt.AuthenticationCallback = @@ -146,7 +145,7 @@ fun runBiometricFlowForGet( getBiometricPromptState) Log.d(TAG, "The BiometricPrompt API call begins for Get.") - runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, + return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, onBiometricFailureFallback, BiometricFlowType.GET, onCancelFlowAndFinish, getBiometricCancellationSignal) } @@ -169,11 +168,11 @@ fun runBiometricFlowForCreate( getBiometricCancellationSignal: () -> CancellationSignal, createRequestDisplayInfo: com.android.credentialmanager.createflow .RequestDisplayInfo? = null, - createProviderInfo: EnabledProviderInfo? = null, -) { + createProviderInfo: EnabledProviderInfo? = null +): Boolean { if (getBiometricPromptState() != BiometricPromptState.INACTIVE) { // Screen is already up, do not re-launch - return + return false } onBiometricPromptStateChange(BiometricPromptState.PENDING) val biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo( @@ -184,7 +183,7 @@ fun runBiometricFlowForCreate( if (biometricDisplayInfo == null) { onBiometricFailureFallback(BiometricFlowType.CREATE) - return + return false } val callback: BiometricPrompt.AuthenticationCallback = @@ -193,7 +192,7 @@ fun runBiometricFlowForCreate( getBiometricPromptState) Log.d(TAG, "The BiometricPrompt API call begins for Create.") - runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, + return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, onBiometricFailureFallback, BiometricFlowType.CREATE, onCancelFlowAndFinish, getBiometricCancellationSignal) } @@ -206,19 +205,19 @@ fun runBiometricFlowForCreate( * only device credentials are requested. */ private fun runBiometricFlow( - context: Context, - biometricDisplayInfo: BiometricDisplayInfo, - callback: BiometricPrompt.AuthenticationCallback, - openMoreOptionsPage: () -> Unit, - onBiometricFailureFallback: (BiometricFlowType) -> Unit, - biometricFlowType: BiometricFlowType, - onCancelFlowAndFinish: () -> Unit, - getBiometricCancellationSignal: () -> CancellationSignal, -) { + context: Context, + biometricDisplayInfo: BiometricDisplayInfo, + callback: BiometricPrompt.AuthenticationCallback, + openMoreOptionsPage: () -> Unit, + onBiometricFailureFallback: (BiometricFlowType) -> Unit, + biometricFlowType: BiometricFlowType, + onCancelFlowAndFinish: () -> Unit, + getBiometricCancellationSignal: () -> CancellationSignal +): Boolean { try { if (!canCallBiometricPrompt(biometricDisplayInfo, context)) { onBiometricFailureFallback(biometricFlowType) - return + return false } val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, @@ -239,7 +238,9 @@ private fun runBiometricFlow( } catch (e: IllegalArgumentException) { Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n") onBiometricFailureFallback(biometricFlowType) + return false } + return true } private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Int? { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 7d61f73a525b..4993a1fa0672 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -123,7 +123,8 @@ fun CreateCredentialScreen( onBiometricPromptStateChange = viewModel::onBiometricPromptStateChange, getBiometricCancellationSignal = - viewModel::getBiometricCancellationSignal + viewModel::getBiometricCancellationSignal, + onLog = { viewModel.logUiEvent(it) }, ) CreateScreenState.MORE_OPTIONS_SELECTION_ONLY -> MoreOptionsSelectionCard( requestDisplayInfo = createCredentialUiState.requestDisplayInfo, @@ -642,12 +643,13 @@ internal fun BiometricSelectionPage( getBiometricPromptState: () -> BiometricPromptState, onBiometricPromptStateChange: (BiometricPromptState) -> Unit, getBiometricCancellationSignal: () -> CancellationSignal, + onLog: @Composable (UiEventEnum) -> Unit ) { if (biometricEntry == null) { fallbackToOriginalFlow(BiometricFlowType.CREATE) return } - runBiometricFlowForCreate( + val biometricFlowCalled = runBiometricFlowForCreate( biometricEntry = biometricEntry, context = LocalContext.current, openMoreOptionsPage = onMoreOptionSelected, @@ -659,6 +661,9 @@ internal fun BiometricSelectionPage( createProviderInfo = enabledProviderInfo, onBiometricFailureFallback = fallbackToOriginalFlow, onIllegalStateAndFinish = onIllegalScreenStateAndFinish, - getBiometricCancellationSignal = getBiometricCancellationSignal, + getBiometricCancellationSignal = getBiometricCancellationSignal ) + if (biometricFlowCalled) { + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED) + } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index ba61b90fa4dc..517ad0069f85 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -166,7 +166,8 @@ fun GetCredentialScreen( onBiometricPromptStateChange = viewModel::onBiometricPromptStateChange, getBiometricCancellationSignal = - viewModel::getBiometricCancellationSignal + viewModel::getBiometricCancellationSignal, + onLog = { viewModel.logUiEvent(it) }, ) } else if (credmanBiometricApiEnabled() && getCredentialUiState.currentScreenState @@ -260,12 +261,13 @@ internal fun BiometricSelectionPage( getBiometricPromptState: () -> BiometricPromptState, onBiometricPromptStateChange: (BiometricPromptState) -> Unit, getBiometricCancellationSignal: () -> CancellationSignal, + onLog: @Composable (UiEventEnum) -> Unit, ) { if (biometricEntry == null) { fallbackToOriginalFlow(BiometricFlowType.GET) return } - runBiometricFlowForGet( + val biometricFlowCalled = runBiometricFlowForGet( biometricEntry = biometricEntry, context = LocalContext.current, openMoreOptionsPage = onMoreOptionSelected, @@ -280,6 +282,9 @@ internal fun BiometricSelectionPage( onBiometricFailureFallback = fallbackToOriginalFlow, getBiometricCancellationSignal = getBiometricCancellationSignal ) + if (biometricFlowCalled) { + onLog(GetCredentialEvent.CREDMAN_GET_CRED_BIOMETRIC_FLOW_LAUNCHED) + } } /** Draws the primary credential selection page, used in Android U. */ diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt index daa42be020ce..39f2fcee6a0a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager.logging import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID enum class CreateCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum { @@ -52,7 +53,10 @@ enum class CreateCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnu CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION(1327), @UiEvent(doc = "The more about passkeys intro card is visible on screen.") - CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328); + CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328), + + @UiEvent(doc = "The single tap biometric flow is launched.") + CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED(RESERVE_NEW_UI_EVENT_ID); override fun getId(): Int { return this.id diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt index 8de8895e8ffc..89fd72cdc8cc 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager.logging import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID enum class GetCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum { @@ -54,7 +55,10 @@ enum class GetCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum { CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD(1341), @UiEvent(doc = "The all sign in option card is visible on screen.") - CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342); + CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342), + + @UiEvent(doc = "The single tap biometric flow is launched.") + CREDMAN_GET_CRED_BIOMETRIC_FLOW_LAUNCHED(RESERVE_NEW_UI_EVENT_ID); override fun getId(): Int { return this.id diff --git a/packages/SettingsLib/Color/res/values/colors.xml b/packages/SettingsLib/Color/res/values/colors.xml index ef0dd1b654b9..b0b9b10952b8 100644 --- a/packages/SettingsLib/Color/res/values/colors.xml +++ b/packages/SettingsLib/Color/res/values/colors.xml @@ -17,7 +17,6 @@ <resources> <!-- Dynamic colors--> - <color name="settingslib_color_blue700">#0B57D0</color> <color name="settingslib_color_blue600">#1a73e8</color> <color name="settingslib_color_blue400">#669df6</color> <color name="settingslib_color_blue300">#8ab4f8</color> diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java index bc3488fc31fb..0447ef8357eb 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java @@ -56,9 +56,6 @@ public class LottieColorUtils { ".black", android.R.color.white); map.put( - ".blue200", - R.color.settingslib_color_blue700); - map.put( ".blue400", R.color.settingslib_color_blue600); map.put( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 9dd3d39cb040..1f7f07bb072d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -28,7 +28,9 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -445,6 +447,14 @@ private fun BoxScope.CommunalHubLazyGrid( val selected by remember(index) { derivedStateOf { list[index].key == selectedKey.value } } DraggableItem( + modifier = + if (dragDropState.draggingItemIndex == index) { + Modifier + } else { + Modifier.animateItem( + placementSpec = spring(stiffness = Spring.StiffnessMediumLow) + ) + }, dragDropState = dragDropState, selected = selected, enabled = list[index].isWidgetContent(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt index ac5004e16a3b..580aba586ee1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.ui.composable +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -56,17 +57,19 @@ fun VolumePanelComposeScope.HorizontalVolumePanelContent( with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) } } } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(spacing), - ) { - for (component in layout.footerComponents) { - AnimatedVisibility( - visible = component.isVisible, - modifier = Modifier.weight(1f), - ) { - with(component.component as ComposeVolumePanelUiComponent) { - Content(Modifier) + AnimatedContent( + targetState = layout.footerComponents, + label = "FooterComponentAnimation", + ) { footerComponents -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing), + ) { + for (component in footerComponents) { + if (component.isVisible) { + with(component.component as ComposeVolumePanelUiComponent) { + Content(Modifier.weight(1f)) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt index 9ea20b9da4b6..6349c1406a12 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.ui.composable +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -50,26 +51,27 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) } } } - if (layout.footerComponents.isNotEmpty()) { + + AnimatedContent( + targetState = layout.footerComponents, + label = "FooterComponentAnimation", + ) { footerComponents -> Row( modifier = Modifier.fillMaxWidth().wrapContentHeight(), horizontalArrangement = Arrangement.spacedBy(if (isLargeScreen) 28.dp else 20.dp), ) { val visibleComponentsCount = - layout.footerComponents.fastSumBy { if (it.isVisible) 1 else 0 } + footerComponents.fastSumBy { if (it.isVisible) 1 else 0 } // Center footer component if there is only one present if (visibleComponentsCount == 1) { Spacer(modifier = Modifier.weight(0.5f)) } - for (component in layout.footerComponents) { - AnimatedVisibility( - visible = component.isVisible, - modifier = Modifier.weight(1f), - ) { + for (component in footerComponents) { + if (component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { - Content(Modifier) + Content(Modifier.weight(1f)) } } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java index 165e9728b9d7..de9baa59b2b7 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java +++ b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java @@ -79,6 +79,21 @@ public class Assert { } } + /** + * Asserts that the current thread is the same as the given thread, or that the current thread + * is the test thread. + * @param expected The looper we expected to be running on + */ + public static void isCurrentThread(Looper expected) { + if (!expected.isCurrentThread() + && (sTestThread == null || sTestThread != Thread.currentThread())) { + throw new IllegalStateException("Called on wrong thread thread." + + " wanted " + expected.getThread().getName() + + " but instead got Thread.currentThread()=" + + Thread.currentThread().getName()); + } + } + public static void isNotMainThread() { if (sMainLooper.isCurrentThread() && (sTestThread == null || sTestThread == Thread.currentThread())) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt new file mode 100644 index 000000000000..7934b02126bd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt @@ -0,0 +1,95 @@ +/* + * 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.ui.navigation + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.uiEventLoggerFake +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.activityStarter +import com.android.systemui.testKosmos +import com.android.systemui.volume.domain.model.VolumePanelRoute +import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor +import com.android.systemui.volume.panel.ui.viewmodel.volumePanelViewModelFactory +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class VolumeNavigatorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val underTest: VolumeNavigator = + with(kosmos) { + VolumeNavigator( + testScope.backgroundScope, + testDispatcher, + mock {}, + activityStarter, + volumePanelViewModelFactory, + mock { + on { create(any(), anyInt(), anyBoolean(), any()) }.thenReturn(mock {}) + on { applicationContext }.thenReturn(context) + }, + uiEventLoggerFake, + volumePanelGlobalStateInteractor, + ) + } + + @Test + fun showNewVolumePanel_keyguardLocked_notShown() = + with(kosmos) { + testScope.runTest { + val panelState by collectLastValue(volumePanelGlobalStateInteractor.globalState) + + underTest.openVolumePanel(VolumePanelRoute.COMPOSE_VOLUME_PANEL) + runCurrent() + + assertThat(panelState!!.isVisible).isFalse() + } + } + + @Test + fun showNewVolumePanel_keyguardUnlocked_shown() = + with(kosmos) { + testScope.runTest { + whenever(activityStarter.dismissKeyguardThenExecute(any(), any(), anyBoolean())) + .then { (it.arguments[0] as ActivityStarter.OnDismissAction).onDismiss() } + val panelState by collectLastValue(volumePanelGlobalStateInteractor.globalState) + + underTest.openVolumePanel(VolumePanelRoute.COMPOSE_VOLUME_PANEL) + runCurrent() + + assertThat(panelState!!.isVisible).isTrue() + } + } +} diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml index 292e49610e2a..06d1bf4c01cb 100644 --- a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml +++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml @@ -5,9 +5,9 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <LinearLayout + <FrameLayout android:id="@+id/shortcut_helper_sheet" - style="@style/Widget.Material3.BottomSheet" + style="@style/ShortcutHelperBottomSheet" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" @@ -19,13 +19,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> - <TextView + <androidx.compose.ui.platform.ComposeView + android:id="@+id/shortcut_helper_compose_container" android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:gravity="center" - android:textAppearance="?textAppearanceDisplayLarge" - android:background="?colorTertiaryContainer" - android:text="Shortcut Helper Content" /> - </LinearLayout> -</androidx.coordinatorlayout.widget.CoordinatorLayout> + android:layout_height="match_parent" /> + </FrameLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/face_dialog_authenticating.json b/packages/SystemUI/res/raw/face_dialog_authenticating.json deleted file mode 100644 index 4e25e6d933c4..000000000000 --- a/packages/SystemUI/res/raw/face_dialog_authenticating.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.13","fr":60,"ip":0,"op":61,"w":64,"h":64,"nm":"face_scanning 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".blue200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[32,32,0],"ix":2,"l":2},"a":{"a":0,"k":[27.25,27.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[95,95,100]},{"t":60,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.243],[-1.244,0],[0,1.243],[1.242,0]],"o":[[0,1.243],[1.242,0],[0,-1.243],[-1.244,0]],"v":[[-2.249,0.001],[0.001,2.251],[2.249,0.001],[0.001,-2.251]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[15.1,20.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.243],[-1.242,0],[0,1.243],[1.242,0]],"o":[[0,1.243],[1.242,0],[0,-1.243],[-1.242,0]],"v":[[-2.249,0],[0.001,2.25],[2.249,0],[0.001,-2.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[39.4,20.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.814,3.523],[-2.814,3.523],[-2.814,1.363],[0.652,1.363],[0.652,-3.523],[2.814,-3.523]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.791,28.479],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.154,0.15],[0,0],[0.117,-0.095],[0,0],[0.228,-0.121],[0.358,-0.103],[0.922,0.261],[0.3,0.16],[0.24,0.185],[0.14,0.139],[0.178,0.261],[0.143,0.451],[0,0],[0,0.494],[0,0],[-0.214,-0.676],[-0.392,-0.572],[-0.323,-0.317],[-0.228,-0.177],[-0.333,-0.179],[-0.503,-0.145],[-0.662,0],[-0.653,0.184],[-0.437,0.233],[-0.336,0.258],[0,0],[0,0]],"o":[[0,0],[-0.107,0.106],[0,0],[-0.24,0.185],[-0.301,0.16],[-0.92,0.261],[-0.357,-0.103],[-0.228,-0.121],[-0.158,-0.122],[-0.225,-0.221],[-0.272,-0.393],[0,0],[-0.147,-0.466],[0,0],[0,0.716],[0.206,0.656],[0.256,0.372],[0.204,0.201],[0.336,0.258],[0.436,0.233],[0.655,0.184],[0.662,0],[0.503,-0.145],[0.332,-0.179],[0,0],[0,0],[0.165,-0.136]],"v":[[6.094,1.465],[4.579,-0.076],[4.242,0.225],[4.124,0.315],[3.43,0.771],[2.439,1.165],[-0.342,1.165],[-1.331,0.771],[-2.027,0.315],[-2.48,-0.075],[-3.087,-0.801],[-3.712,-2.075],[-3.712,-2.075],[-3.934,-3.523],[-6.094,-3.523],[-5.771,-1.424],[-4.868,0.424],[-3.995,1.465],[-3.344,2.027],[-2.35,2.676],[-0.934,3.243],[1.049,3.523],[3.031,3.243],[4.449,2.676],[5.441,2.027],[5.482,1.997],[5.615,1.895]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[26.201,40.411],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.398,0],[0,-13.4],[13.398,0],[0,13.4]],"o":[[13.398,0],[0,13.4],[-13.398,0],[0,-13.4]],"v":[[0,-24.3],[24.3,0],[0,24.3],[-24.3,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[14.904,0],[0,-14.904],[-14.904,0],[0,14.904]],"o":[[-14.904,0],[0,14.904],[14.904,0],[0,-14.904]],"v":[[0,-27],[-27,0],[0,27],[27,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.25,27.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":4,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1200,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c038a8207d43..1226bbf21a0f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3489,6 +3489,45 @@ <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] --> <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string> + <!-- Title of the keyboard shortcut helper category "System". The helper is a component that + shows the user which keyboard shortcuts they can use. The "System" shortcuts are for + example "Take a screenshot" or "Go back". [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_system">System</string> + <!-- Title of the keyboard shortcut helper category "Multitasking". The helper is a component + that shows the user which keyboard shortcuts they can use. The "Multitasking" shortcuts are + for example "Enter split screen". [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_multitasking">Multitasking</string> + <!-- Title of the keyboard shortcut helper category "Input". The helper is a component + that shows the user which keyboard shortcuts they can use. The "Input" shortcuts are + the ones provided by the keyboard. Examples are "Access emoji" or "Switch to next language" + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_input">Input</string> + <!-- Title of the keyboard shortcut helper category "App shortcuts". The helper is a component + that shows the user which keyboard shortcuts they can use. The "App shortcuts" are + for example "Open browser" or "Open calculator". [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_app_shortcuts">App shortcuts</string> + <!-- Title of the keyboard shortcut helper category "Accessibility". The helper is a component + that shows the user which keyboard shortcuts they can use. The "Accessibility" shortcuts + are for example "Turn on talkback". [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_a11y">Accessibility</string> + <!-- Title at the top of the keyboard shortcut helper UI. The helper is a component + that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_title">Keyboard shortcuts</string> + <!-- Placeholder text shown in the search box of the keyboard shortcut helper, when the user + hasn't typed in anything in the search box yet. The helper is a component that shows the + user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_search_placeholder">Search shortcuts</string> + <!-- Content description of the icon that allows to collapse a keyboard shortcut helper category + panel. The helper is a component that shows the user which keyboard shortcuts they can + use. The helper shows shortcuts in categories, which can be collapsed or expanded. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_content_description_collapse_icon">Collapse icon</string> + <!-- Content description of the icon that allows to expand a keyboard shortcut helper category + panel. The helper is a component that shows the user which keyboard shortcuts they can + use. The helper shows shortcuts in categories, which can be collapsed or expanded. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_content_description_expand_icon">Expand icon</string> + <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] --> <string name="keyboard_backlight_dialog_title">Keyboard backlight</string> <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 64717fcc8c5d..1e0adec4e84f 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1665,6 +1665,10 @@ <item name="android:colorBackground">@color/transparent</item> </style> + <style name="ShortcutHelperBottomSheet" parent="@style/Widget.Material3.BottomSheet"> + <item name="backgroundTint">?colorSurfaceContainer</item> + </style> + <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity"> <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item> <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt index a74b0b07299c..b8ff3bb43203 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt @@ -98,11 +98,11 @@ constructor( ) { unscaledSensorLocation, scale -> val sensorLocation = SensorLocation( - unscaledSensorLocation.sensorLocationX, - unscaledSensorLocation.sensorLocationY, - unscaledSensorLocation.sensorRadius, + naturalCenterX = unscaledSensorLocation.sensorLocationX, + naturalCenterY = unscaledSensorLocation.sensorLocationY, + naturalRadius = unscaledSensorLocation.sensorRadius, + scale = scale ) - sensorLocation.scale = scale sensorLocation } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt index dddadbd5e036..2f2f3a35dbaa 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt @@ -16,18 +16,18 @@ package com.android.systemui.biometrics.shared.model -/** Provides current sensor location information in the current screen resolution [scale]. */ +/** + * Provides current sensor location information in the current screen resolution [scale]. + * + * @property scale Scale to apply to the sensor location's natural parameters to support different + * screen resolutions. + */ data class SensorLocation( private val naturalCenterX: Int, private val naturalCenterY: Int, - private val naturalRadius: Int + private val naturalRadius: Int, + private val scale: Float = 1f ) { - /** - * Scale to apply to the sensor location's natural parameters to support different screen - * resolutions. - */ - var scale: Float = 1f - val centerX: Float get() { return naturalCenterX * scale diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt index fcc69927c2b3..9e836c31c177 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -17,7 +17,9 @@ package com.android.systemui.biometrics.ui.binder +import android.graphics.drawable.Animatable2 import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView @@ -28,8 +30,8 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.res.R import com.android.systemui.util.kotlin.Utils.Companion.toQuad +import com.android.systemui.util.kotlin.Utils.Companion.toQuint import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import kotlinx.coroutines.flow.combine @@ -61,6 +63,16 @@ object PromptIconViewBinder { } var faceIcon: AnimatedVectorDrawable? = null + val faceIconCallback = + object : Animatable2.AnimationCallback() { + override fun onAnimationStart(drawable: Drawable) { + viewModel.onAnimationStart() + } + + override fun onAnimationEnd(drawable: Drawable) { + viewModel.onAnimationEnd() + } + } if (!constraintBp()) { launch { @@ -126,13 +138,19 @@ object PromptIconViewBinder { combine( viewModel.activeAuthType, viewModel.shouldAnimateIconView, + viewModel.shouldRepeatAnimation, viewModel.showingError, - ::Triple + ::toQuad ), - ::toQuad + ::toQuint ) - .collect { (iconAsset, activeAuthType, shouldAnimateIconView, showingError) - -> + .collect { + ( + iconAsset, + activeAuthType, + shouldAnimateIconView, + shouldRepeatAnimation, + showingError) -> if (iconAsset != -1) { when (activeAuthType) { AuthType.Fingerprint, @@ -145,27 +163,21 @@ object PromptIconViewBinder { } } AuthType.Face -> { - // TODO(b/318569643): Consolidate logic once all face auth - // assets are migrated from drawable to json - if (iconAsset == R.raw.face_dialog_authenticating) { - iconView.setAnimation(iconAsset) - iconView.frame = 0 - + faceIcon?.apply { + unregisterAnimationCallback(faceIconCallback) + stop() + } + faceIcon = + iconView.context.getDrawable(iconAsset) + as AnimatedVectorDrawable + faceIcon?.apply { + iconView.setImageDrawable(this) if (shouldAnimateIconView) { - iconView.playAnimation() - iconView.loop(true) - } - } else { - faceIcon?.apply { stop() } - faceIcon = - iconView.context.getDrawable(iconAsset) - as AnimatedVectorDrawable - faceIcon?.apply { - iconView.setImageDrawable(this) - if (shouldAnimateIconView) { - forceAnimationOnUI() - start() + forceAnimationOnUI() + if (shouldRepeatAnimation) { + registerAnimationCallback(faceIconCallback) } + start() } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt index 901d7517c5e9..bde3e992a295 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt @@ -21,6 +21,7 @@ import android.annotation.DrawableRes import android.annotation.RawRes import android.content.res.Configuration import android.graphics.Rect +import android.hardware.face.Face import android.util.RotationUtils import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor @@ -31,10 +32,12 @@ import com.android.systemui.res.R import com.android.systemui.util.kotlin.combine import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map /** * Models UI of [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay] @@ -55,8 +58,11 @@ constructor( } /** - * Indicates what auth type the UI currently displays. Fingerprint-only auth -> Fingerprint - * Face-only auth -> Face Co-ex auth, implicit flow -> Face Co-ex auth, explicit flow -> Coex + * Indicates what auth type the UI currently displays. + * Fingerprint-only auth -> Fingerprint + * Face-only auth -> Face + * Co-ex auth, implicit flow -> Face + * Co-ex auth, explicit flow -> Coex */ val activeAuthType: Flow<AuthType> = combine( @@ -113,6 +119,35 @@ constructor( _previousIconOverlayWasError.value = previousIconOverlayWasError } + /** Called when iconView begins animating. */ + fun onAnimationStart() { + _animationEnded.value = false + } + + /** Called when iconView ends animating. */ + fun onAnimationEnd() { + _animationEnded.value = true + } + + private val _animationEnded: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** + * Whether a face iconView should pulse (i.e. while isAuthenticating and previous animation + * ended). + */ + val shouldPulseAnimation: Flow<Boolean> = + combine(_animationEnded, promptViewModel.isAuthenticating) { + animationEnded, + isAuthenticating -> + animationEnded && isAuthenticating + } + .distinctUntilChanged() + + private val _lastPulseLightToDark: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** Tracks whether a face iconView last pulsed light to dark (vs. dark to light) */ + val lastPulseLightToDark: Flow<Boolean> = _lastPulseLightToDark.asStateFlow() + val iconSize: Flow<Pair<Int, Int>> = combine( promptViewModel.position, @@ -160,22 +195,35 @@ constructor( } } AuthType.Face -> - combine( - promptViewModel.isAuthenticated.distinctUntilChanged(), - promptViewModel.isAuthenticating.distinctUntilChanged(), - promptViewModel.isPendingConfirmation.distinctUntilChanged(), - promptViewModel.showingError.distinctUntilChanged() - ) { - authState: PromptAuthState, - isAuthenticating: Boolean, - isPendingConfirmation: Boolean, - showingError: Boolean -> - getFaceIconViewAsset( - authState, - isAuthenticating, - isPendingConfirmation, - showingError - ) + shouldPulseAnimation.flatMapLatest { shouldPulseAnimation: Boolean -> + if (shouldPulseAnimation) { + val iconAsset = + if (_lastPulseLightToDark.value) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + _lastPulseLightToDark.value = !_lastPulseLightToDark.value + flowOf(iconAsset) + } else { + combine( + promptViewModel.isAuthenticated.distinctUntilChanged(), + promptViewModel.isAuthenticating.distinctUntilChanged(), + promptViewModel.isPendingConfirmation.distinctUntilChanged(), + promptViewModel.showingError.distinctUntilChanged() + ) { + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + getFaceIconViewAsset( + authState, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + } } AuthType.Coex -> combine( @@ -279,7 +327,8 @@ constructor( } else if (authState.isAuthenticated) { R.drawable.face_dialog_dark_to_checkmark } else if (isAuthenticating) { - R.raw.face_dialog_authenticating + _lastPulseLightToDark.value = false + R.drawable.face_dialog_pulse_dark_to_light } else if (showingError) { R.drawable.face_dialog_dark_to_error } else if (_previousIconWasError.value) { @@ -654,6 +703,16 @@ constructor( } } + /** Whether the current BiometricPromptLayout.iconView asset animation should be repeated. */ + val shouldRepeatAnimation: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> flowOf(false) + AuthType.Face -> promptViewModel.isAuthenticating.map { it } + } + } + /** Called on configuration changes */ fun onConfigurationChanged(newConfig: Configuration) { displayStateInteractor.onConfigurationChanged(newConfig) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt new file mode 100644 index 000000000000..52ccc219353e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -0,0 +1,413 @@ +/* + * 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.keyboard.shortcut.ui.composable + +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Accessibility +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Tv +import androidx.compose.material.icons.filled.VerticalSplit +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationDrawerItemColors +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed +import com.android.compose.windowsizeclass.LocalWindowSizeClass +import com.android.systemui.res.R + +@Composable +fun ShortcutHelper(modifier: Modifier = Modifier, onKeyboardSettingsClicked: () -> Unit) { + if (shouldUseSinglePane()) { + ShortcutHelperSinglePane(modifier, categories, onKeyboardSettingsClicked) + } else { + ShortcutHelperTwoPane(modifier, categories, onKeyboardSettingsClicked) + } +} + +@Composable +private fun shouldUseSinglePane() = + LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact + +@Composable +private fun ShortcutHelperSinglePane( + modifier: Modifier = Modifier, + categories: List<ShortcutHelperCategory>, + onKeyboardSettingsClicked: () -> Unit, +) { + Column( + modifier = + modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(start = 16.dp, end = 16.dp, top = 26.dp) + ) { + TitleBar() + Spacer(modifier = Modifier.height(6.dp)) + ShortcutsSearchBar() + Spacer(modifier = Modifier.height(16.dp)) + CategoriesPanelSinglePane(categories) + Spacer(modifier = Modifier.weight(1f)) + KeyboardSettings(onClick = onKeyboardSettingsClicked) + } +} + +@Composable +private fun CategoriesPanelSinglePane( + categories: List<ShortcutHelperCategory>, +) { + var expandedCategory by remember { mutableStateOf<ShortcutHelperCategory?>(null) } + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + categories.fastForEachIndexed { index, category -> + val isExpanded = expandedCategory == category + val itemShape = + if (index == 0) { + ShortcutHelper.Shapes.singlePaneFirstCategory + } else if (index == categories.lastIndex) { + ShortcutHelper.Shapes.singlePaneLastCategory + } else { + ShortcutHelper.Shapes.singlePaneCategory + } + CategoryItemSinglePane( + category = category, + isExpanded = isExpanded, + onClick = { + expandedCategory = + if (isExpanded) { + null + } else { + category + } + }, + shape = itemShape, + ) + } + } +} + +@Composable +private fun CategoryItemSinglePane( + category: ShortcutHelperCategory, + isExpanded: Boolean, + onClick: () -> Unit, + shape: Shape, +) { + Surface( + color = MaterialTheme.colorScheme.surfaceBright, + shape = shape, + onClick = onClick, + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp) + ) { + Icon(category.icon, contentDescription = null) + Spacer(modifier = Modifier.width(16.dp)) + Text(stringResource(category.labelResId)) + Spacer(modifier = Modifier.weight(1f)) + RotatingExpandCollapseIcon(isExpanded) + } + AnimatedVisibility(visible = isExpanded) { ShortcutCategoryDetailsSinglePane(category) } + } + } +} + +@Composable +private fun RotatingExpandCollapseIcon(isExpanded: Boolean) { + val expandIconRotationDegrees by + animateFloatAsState( + targetValue = + if (isExpanded) { + 180f + } else { + 0f + }, + label = "Expand icon rotation animation" + ) + Icon( + modifier = + Modifier.background( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = CircleShape + ) + .graphicsLayer { rotationZ = expandIconRotationDegrees }, + imageVector = Icons.Default.ExpandMore, + contentDescription = + if (isExpanded) { + stringResource(R.string.shortcut_helper_content_description_collapse_icon) + } else { + stringResource(R.string.shortcut_helper_content_description_expand_icon) + }, + tint = MaterialTheme.colorScheme.onSurface + ) +} + +@Composable +private fun ShortcutCategoryDetailsSinglePane(category: ShortcutHelperCategory) { + Box(modifier = Modifier.fillMaxWidth().heightIn(min = 300.dp)) { + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(category.labelResId), + ) + } +} + +@Composable +private fun ShortcutHelperTwoPane( + modifier: Modifier = Modifier, + categories: List<ShortcutHelperCategory>, + onKeyboardSettingsClicked: () -> Unit, +) { + Column(modifier = modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 26.dp)) { + TitleBar() + Spacer(modifier = Modifier.height(12.dp)) + Row(Modifier.fillMaxWidth()) { + StartSidePanel( + modifier = Modifier.fillMaxWidth(fraction = 0.32f), + categories = categories, + onKeyboardSettingsClicked = onKeyboardSettingsClicked, + ) + Spacer(modifier = Modifier.width(24.dp)) + EndSidePanel(Modifier.fillMaxSize()) + } + } +} + +@Composable +private fun StartSidePanel( + modifier: Modifier, + categories: List<ShortcutHelperCategory>, + onKeyboardSettingsClicked: () -> Unit, +) { + Column(modifier) { + ShortcutsSearchBar() + Spacer(modifier = Modifier.heightIn(16.dp)) + CategoriesPanelTwoPane(categories) + Spacer(modifier = Modifier.weight(1f)) + KeyboardSettings(onKeyboardSettingsClicked) + } +} + +@Composable +private fun CategoriesPanelTwoPane(categories: List<ShortcutHelperCategory>) { + var selected by remember { mutableStateOf(categories.first()) } + Column { + categories.fastForEach { + CategoryItemTwoPane( + label = stringResource(it.labelResId), + icon = it.icon, + selected = selected == it, + onClick = { selected = it } + ) + } + } +} + +@Composable +private fun CategoryItemTwoPane( + label: String, + icon: ImageVector, + selected: Boolean, + onClick: () -> Unit, + colors: NavigationDrawerItemColors = + NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent), +) { + Surface( + selected = selected, + onClick = onClick, + modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 72.dp).fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = colors.containerColor(selected).value, + ) { + Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = icon, + contentDescription = null, + tint = colors.iconColor(selected).value + ) + Spacer(Modifier.width(12.dp)) + Box(Modifier.weight(1f)) { + Text( + fontSize = 18.sp, + color = colors.textColor(selected).value, + style = MaterialTheme.typography.headlineSmall, + text = label + ) + } + } + } +} + +@Composable +fun EndSidePanel(modifier: Modifier) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceBright + ) {} +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun TitleBar() { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent), + title = { + Text( + text = stringResource(R.string.shortcut_helper_title), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineSmall + ) + } + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun ShortcutsSearchBar() { + var query by remember { mutableStateOf("") } + SearchBar( + colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright), + query = query, + active = false, + onActiveChange = {}, + onQueryChange = { query = it }, + onSearch = {}, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) }, + content = {} + ) +} + +@Composable +private fun KeyboardSettings(onClick: () -> Unit) { + Surface( + onClick = onClick, + shape = RoundedCornerShape(24.dp), + color = Color.Transparent, + modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Keyboard Settings", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 16.sp + ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.AutoMirrored.Default.OpenInNew, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +/** Temporary data class just to populate the UI. */ +private data class ShortcutHelperCategory( + @StringRes val labelResId: Int, + val icon: ImageVector, +) + +// Temporarily populating the categories directly in the UI. +private val categories = + listOf( + ShortcutHelperCategory(R.string.shortcut_helper_category_system, Icons.Default.Tv), + ShortcutHelperCategory( + R.string.shortcut_helper_category_multitasking, + Icons.Default.VerticalSplit + ), + ShortcutHelperCategory(R.string.shortcut_helper_category_input, Icons.Default.Keyboard), + ShortcutHelperCategory(R.string.shortcut_helper_category_app_shortcuts, Icons.Default.Apps), + ShortcutHelperCategory(R.string.shortcut_helper_category_a11y, Icons.Default.Accessibility), + ) + +object ShortcutHelper { + + object Shapes { + val singlePaneFirstCategory = + RoundedCornerShape( + topStart = Dimensions.SinglePaneCategoryCornerRadius, + topEnd = Dimensions.SinglePaneCategoryCornerRadius + ) + val singlePaneLastCategory = + RoundedCornerShape( + bottomStart = Dimensions.SinglePaneCategoryCornerRadius, + bottomEnd = Dimensions.SinglePaneCategoryCornerRadius + ) + val singlePaneCategory = RectangleShape + } + + object Dimensions { + val SinglePaneCategoryCornerRadius = 28.dp + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt index ef4156da4f7b..1e8d23918964 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt @@ -23,9 +23,12 @@ import android.view.WindowInsets import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback +import androidx.compose.ui.platform.ComposeView import androidx.core.view.updatePadding import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import com.android.compose.theme.PlatformTheme +import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel import com.android.systemui.res.R import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -58,14 +61,30 @@ constructor( super.onCreate(savedInstanceState) setContentView(R.layout.activity_keyboard_shortcut_helper) setUpBottomSheetWidth() + expandBottomSheet() setUpInsets() setUpPredictiveBack() setUpSheetDismissListener() setUpDismissOnTouchOutside() + setUpComposeView() observeFinishRequired() viewModel.onViewOpened() } + private fun setUpComposeView() { + requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply { + setContent { + PlatformTheme { + ShortcutHelper( + onKeyboardSettingsClicked = ::onKeyboardSettingsClicked, + ) + } + } + } + } + + private fun onKeyboardSettingsClicked() {} + override fun onDestroy() { super.onDestroy() if (isFinishing) { @@ -101,7 +120,8 @@ constructor( bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets -> val safeDrawingInsets = insets.safeDrawing // Make sure the bottom sheet is not covered by the status bar. - bottomSheetContainer.updatePadding(top = safeDrawingInsets.top) + bottomSheetBehavior.maxHeight = + resources.displayMetrics.heightPixels - safeDrawingInsets.top // Make sure the contents inside of the bottom sheet are not hidden by system bars, or // cutouts. bottomSheet.updatePadding( @@ -171,7 +191,6 @@ constructor( private val WindowInsets.safeDrawing get() = getInsets(WindowInsets.Type.systemBars()) - .union(getInsets(WindowInsets.Type.ime())) .union(getInsets(WindowInsets.Type.displayCutout())) private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index c5fab8f57822..e01f0a152b37 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -147,8 +147,9 @@ constructor( deviceEntryIconViewModel.get().udfpsLocation.value?.let { udfpsLocation -> Log.d( "DeviceEntrySection", - "udfpsLocation=$udfpsLocation" + - " unusedAuthController=${authController.udfpsLocation}" + "udfpsLocation=$udfpsLocation, " + + "scaledLocation=(${udfpsLocation.centerX},${udfpsLocation.centerY}), " + + "unusedAuthController=${authController.udfpsLocation}" ) centerIcon( Point(udfpsLocation.centerX.toInt(), udfpsLocation.centerY.toInt()), diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt index e0c54190283a..9c29bab80d14 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt @@ -43,6 +43,7 @@ interface MediaDomainModule { @IntoMap @ClassKey(MediaDataProcessor::class) fun bindMediaDataProcessor(interactor: MediaDataProcessor): CoreStartable + companion object { @Provides @@ -52,7 +53,7 @@ interface MediaDomainModule { newProvider: Provider<MediaCarouselInteractor>, mediaFlags: MediaFlags, ): MediaDataManager { - return if (mediaFlags.isMediaControlsRefactorEnabled()) { + return if (mediaFlags.isSceneContainerEnabled()) { newProvider.get() } else { legacyProvider.get() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index eed775242d1f..8e985e11732f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -269,7 +269,7 @@ class MediaDataProcessor( } override fun start() { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { return } @@ -746,8 +746,7 @@ class MediaDataProcessor( notif.extras.getParcelable( Notification.EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo::class.java - ) - ?: getAppInfoFromPackage(sbn.packageName) + ) ?: getAppInfoFromPackage(sbn.packageName) // App name val appName = getAppName(sbn, appInfo) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt index 9e6230012760..b4bd4fd2c266 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt @@ -36,8 +36,8 @@ import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilt import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener import com.android.systemui.media.controls.domain.resume.MediaResumeListener import com.android.systemui.media.controls.shared.model.MediaCommonModel -import com.android.systemui.media.controls.util.MediaControlsRefactorFlag import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.scene.shared.flag.SceneContainerFlag import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -127,7 +127,7 @@ constructor( val currentMedia: StateFlow<List<MediaCommonModel>> = mediaFilterRepository.currentMedia override fun start() { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { return } @@ -256,8 +256,6 @@ constructor( companion object { val unsupported: Nothing get() = - error( - "Code path not supported when ${MediaControlsRefactorFlag.FLAG_NAME} is enabled" - ) + error("Code path not supported when ${SceneContainerFlag.DESCRIPTION} is enabled") } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 19e3e0715989..8316b3aba73e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -217,7 +217,7 @@ constructor( private val animationScaleObserver: ContentObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() } } else { controllerByViewModel.values.forEach { it.updateAnimatorDurationScale() } @@ -347,7 +347,7 @@ constructor( inflateSettingsButton() mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) configurationController.addCallback(configListener) - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { setUpListeners() } else { val visualStabilityCallback = OnReorderingAllowedListener { @@ -389,7 +389,7 @@ constructor( listenForAnyStateToLockscreenTransition(this) listenForLockscreenSettingChanges(this) - if (!mediaFlags.isMediaControlsRefactorEnabled()) return@repeatOnLifecycle + if (!mediaFlags.isSceneContainerEnabled()) return@repeatOnLifecycle listenForMediaItemsChanges(this) } } @@ -882,8 +882,7 @@ constructor( val previousVisibleIndex = MediaPlayerData.playerKeys().indexOfFirst { key -> it == key } mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex) - } - ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex) + } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex) } } else if (isRtl && mediaContent.childCount > 0) { // In RTL, Scroll to the first player as it is the rightmost player in media carousel. @@ -1092,7 +1091,7 @@ constructor( } private fun updatePlayers(recreateMedia: Boolean) { - if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (mediaFlags.isSceneContainerEnabled()) { updateMediaPlayers(recreateMedia) return } @@ -1192,7 +1191,7 @@ constructor( currentStartLocation = startLocation currentEndLocation = endLocation currentTransitionProgress = progress - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { for (mediaPlayer in MediaPlayerData.players()) { updateViewControllerToState(mediaPlayer.mediaViewController, immediately) } @@ -1254,7 +1253,7 @@ constructor( /** Update listening to seekbar. */ private fun updateSeekbarListening(visibleToUser: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { for (player in MediaPlayerData.players()) { player.setListening(visibleToUser && currentlyExpanded) } @@ -1269,7 +1268,7 @@ constructor( private fun updateCarouselDimensions() { var width = 0 var height = 0 - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { for (mediaPlayer in MediaPlayerData.players()) { val controller = mediaPlayer.mediaViewController // When transitioning the view to gone, the view gets smaller, but the translation @@ -1361,7 +1360,7 @@ constructor( !mediaManager.hasActiveMediaOrRecommendation() && desiredHostState.showsOnlyActiveMedia - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { for (mediaPlayer in MediaPlayerData.players()) { if (animate) { mediaPlayer.mediaViewController.animatePendingStateChange( @@ -1401,7 +1400,7 @@ constructor( } fun closeGuts(immediate: Boolean = true) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { MediaPlayerData.players().forEach { it.closeGuts(immediate) } } else { controllerByViewModel.values.forEach { it.closeGuts(immediate) } @@ -1544,7 +1543,7 @@ constructor( @VisibleForTesting fun onSwipeToDismiss() { - if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (mediaFlags.isSceneContainerEnabled()) { mediaCarouselViewModel.onSwipeToDismiss() return } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 38377088a2d7..9d0723211d4b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -203,7 +203,7 @@ constructor( private val scrubbingChangeListener = object : SeekBarViewModel.ScrubbingChangeListener { override fun onScrubbingChanged(scrubbing: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return if (isScrubbing == scrubbing) return isScrubbing = scrubbing updateDisplayForScrubbingChange() @@ -213,7 +213,7 @@ constructor( private val enabledChangeListener = object : SeekBarViewModel.EnabledChangeListener { override fun onEnabledChanged(enabled: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return if (isSeekBarEnabled == enabled) return isSeekBarEnabled = enabled MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled) @@ -229,7 +229,7 @@ constructor( * @param listening True when player should be active. Otherwise, false. */ fun setListening(listening: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return seekBarViewModel.listening = listening } @@ -263,7 +263,7 @@ constructor( ) ) } - if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (mediaFlags.isSceneContainerEnabled()) { if ( this@MediaViewController::recsConfigurationChangeListener.isInitialized ) { @@ -305,6 +305,7 @@ constructor( */ var collapsedLayout = ConstraintSet() @VisibleForTesting set + /** * The expanded constraint set used to render a collapsed player. If it is modified, make sure * to call [refreshState] @@ -334,7 +335,7 @@ constructor( * Notify this controller that the view has been removed and all listeners should be destroyed */ fun onDestroy() { - if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (mediaFlags.isSceneContainerEnabled()) { if (this::seekBarObserver.isInitialized) { seekBarViewModel.progress.removeObserver(seekBarObserver) } @@ -657,7 +658,7 @@ constructor( } fun attachPlayer(mediaViewHolder: MediaViewHolder) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return this.mediaViewHolder = mediaViewHolder // Setting up seek bar. @@ -731,7 +732,7 @@ constructor( } fun updateAnimatorDurationScale() { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return if (this::seekBarObserver.isInitialized) { seekBarObserver.animationEnabled = globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f @@ -787,7 +788,7 @@ constructor( } fun attachRecommendations(recommendationViewHolder: RecommendationViewHolder) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return this.recommendationViewHolder = recommendationViewHolder attach(recommendationViewHolder.recommendations, TYPE.RECOMMENDATION) @@ -796,13 +797,13 @@ constructor( } fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return seekBarViewModel.logSeek = onSeek onBindSeekBar.invoke(seekBarViewModel) } fun setUpTurbulenceNoise() { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return if (!this::turbulenceNoiseAnimationConfig.isInitialized) { turbulenceNoiseAnimationConfig = createTurbulenceNoiseConfig( @@ -1153,13 +1154,13 @@ constructor( } fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return isPrevButtonAvailable = isAvailable prevNotVisibleValue = notVisibleValue } fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return isNextButtonAvailable = isAvailable nextNotVisibleValue = notVisibleValue } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt deleted file mode 100644 index 2850b4bb2358..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.util - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the media_controls_refactor flag state. */ -@Suppress("NOTHING_TO_INLINE") -object MediaControlsRefactorFlag { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_MEDIA_CONTROLS_REFACTOR - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the flag enabled? */ - @JvmStatic - inline val isEnabled - get() = Flags.mediaControlsRefactor() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 1e7bc0cacf1d..21c311191710 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -52,8 +52,4 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlagsClass /** Check whether to use scene framework */ fun isSceneContainerEnabled() = SceneContainerFlag.isEnabled - - /** Check whether to use media refactor code */ - fun isMediaControlsRefactorEnabled() = - MediaControlsRefactorFlag.isEnabled && SceneContainerFlag.isEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index b208434c3218..18358a79cbca 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -36,11 +36,12 @@ import androidx.dynamicanimation.animation.DynamicAnimation import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.ViewController +import com.android.systemui.util.concurrency.BackPanelUiThread +import com.android.systemui.util.concurrency.UiThreadContext import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import javax.inject.Inject @@ -84,11 +85,11 @@ internal constructor( context: Context, private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, + private val mainHandler: Handler, private val systemClock: SystemClock, private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, - private val latencyTracker: LatencyTracker, + latencyTracker: LatencyTracker, private val interactionJankMonitor: InteractionJankMonitor, ) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin { @@ -103,7 +104,7 @@ internal constructor( constructor( private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, + @BackPanelUiThread private val uiThreadContext: UiThreadContext, private val systemClock: SystemClock, private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, @@ -112,20 +113,19 @@ internal constructor( ) { /** Construct a [BackPanelController]. */ fun create(context: Context): BackPanelController { - val backPanelController = - BackPanelController( + uiThreadContext.isCurrentThread() + return BackPanelController( context, windowManager, viewConfiguration, - mainHandler, + uiThreadContext.handler, systemClock, vibratorHelper, configurationController, latencyTracker, interactionJankMonitor ) - backPanelController.init() - return backPanelController + .also { it.init() } } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index d0f8412c85b2..2dc09e5ab478 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -44,8 +44,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; import android.icu.text.SimpleDateFormat; -import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; @@ -55,7 +53,6 @@ import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import android.view.Choreographer; import android.view.ISystemGestureExclusionListener; import android.view.IWindowManager; import android.view.InputDevice; @@ -75,7 +72,6 @@ import androidx.annotation.DimenRes; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.policy.GestureNavigationSettingsObserver; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; @@ -94,7 +90,8 @@ import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.phone.LightBarController; -import com.android.systemui.util.Assert; +import com.android.systemui.util.concurrency.BackPanelUiThread; +import com.android.systemui.util.concurrency.UiThreadContext; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.pip.Pip; @@ -136,7 +133,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack public void onSystemGestureExclusionChanged(int displayId, Region systemGestureExclusion, Region unrestrictedOrNull) { if (displayId == mDisplayId) { - mMainExecutor.execute(() -> { + mUiThreadContext.getExecutor().execute(() -> { mExcludeRegion.set(systemGestureExclusion); mUnrestrictedExcludeRegion.set(unrestrictedOrNull != null ? unrestrictedOrNull : systemGestureExclusion); @@ -215,8 +212,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final Point mDisplaySize = new Point(); private final int mDisplayId; - private final Executor mMainExecutor; - private final Handler mMainHandler; + private final UiThreadContext mUiThreadContext; private final Executor mBackgroundExecutor; private final Rect mPipExcludedBounds = new Rect(); @@ -411,8 +407,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack OverviewProxyService overviewProxyService, SysUiState sysUiState, PluginManager pluginManager, - @Main Executor executor, - @Main Handler handler, + @BackPanelUiThread UiThreadContext uiThreadContext, @Background Executor backgroundExecutor, UserTracker userTracker, NavigationModeController navigationModeController, @@ -428,8 +423,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack Provider<LightBarController> lightBarControllerProvider) { mContext = context; mDisplayId = context.getDisplayId(); - mMainExecutor = executor; - mMainHandler = handler; + mUiThreadContext = uiThreadContext; mBackgroundExecutor = backgroundExecutor; mUserTracker = userTracker; mOverviewProxyService = overviewProxyService; @@ -478,7 +472,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack ViewConfiguration.getLongPressTimeout()); mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver( - mMainHandler, mContext, this::onNavigationSettingsChanged); + mUiThreadContext.getHandler(), mContext, this::onNavigationSettingsChanged); updateCurrentUserResources(); } @@ -564,13 +558,15 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mIsAttached = true; mOverviewProxyService.addCallback(mQuickSwitchListener); mSysUiState.addCallback(mSysUiStateCallback); - mInputManager.registerInputDeviceListener(mInputDeviceListener, mMainHandler); - int [] inputDevices = mInputManager.getInputDeviceIds(); + mInputManager.registerInputDeviceListener( + mInputDeviceListener, + mUiThreadContext.getHandler()); + int[] inputDevices = mInputManager.getInputDeviceIds(); for (int inputDeviceId : inputDevices) { mInputDeviceListener.onInputDeviceAdded(inputDeviceId); } updateIsEnabled(); - mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); + mUserTracker.addCallback(mUserChangedCallback, mUiThreadContext.getExecutor()); } /** @@ -617,6 +613,10 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private void updateIsEnabled() { + mUiThreadContext.runWithScissors(this::updateIsEnabledInner); + } + + private void updateIsEnabledInner() { try { Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled"); @@ -661,12 +661,12 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack TaskStackChangeListeners.getInstance().registerTaskStackListener( mTaskStackListener); DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, - mMainExecutor::execute, mOnPropertiesChangedListener); + mUiThreadContext.getExecutor()::execute, mOnPropertiesChangedListener); mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener( mOnIsInPipStateChangedListener)); mDesktopModeOptional.ifPresent( dm -> dm.addDesktopGestureExclusionRegionListener( - mDesktopCornersChangedListener, mMainExecutor)); + mDesktopCornersChangedListener, mUiThreadContext.getExecutor())); try { mWindowManagerService.registerSystemGestureExclusionListener( @@ -677,8 +677,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // Register input event receiver mInputMonitor = new InputMonitorCompat("edge-swipe", mDisplayId); - mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(), - Choreographer.getInstance(), this::onInputEvent); + mInputEventReceiver = mInputMonitor.getInputReceiver(mUiThreadContext.getLooper(), + mUiThreadContext.getChoreographer(), this::onInputEvent); // Add a nav bar panel window resetEdgeBackPlugin(); @@ -773,7 +773,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mUseMLModel = newState; if (mUseMLModel) { - Assert.isMainThread(); + mUiThreadContext.isCurrentThread(); if (mMLModelIsLoading) { Log.d(TAG, "Model tried to load while already loading."); return; @@ -804,12 +804,13 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } BackGestureTfClassifierProvider finalProvider = provider; Map<String, Integer> finalVocab = vocab; - mMainExecutor.execute(() -> onMLModelLoadFinished(finalProvider, finalVocab, threshold)); + mUiThreadContext.getExecutor().execute( + () -> onMLModelLoadFinished(finalProvider, finalVocab, threshold)); } private void onMLModelLoadFinished(BackGestureTfClassifierProvider provider, Map<String, Integer> vocab, float threshold) { - Assert.isMainThread(); + mUiThreadContext.isCurrentThread(); mMLModelIsLoading = false; if (!mUseMLModel) { // This can happen if the user disables Gesture Nav while the model is loading. @@ -1291,7 +1292,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack updateBackAnimationThresholds(); if (mLightBarControllerProvider.get() != null) { mBackAnimation.setStatusBarCustomizer((appearance) -> { - mMainExecutor.execute(() -> + mUiThreadContext.getExecutor().execute(() -> mLightBarControllerProvider.get() .customizeStatusBarAppearance(appearance)); }); @@ -1308,8 +1309,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final OverviewProxyService mOverviewProxyService; private final SysUiState mSysUiState; private final PluginManager mPluginManager; - private final Executor mExecutor; - private final Handler mHandler; + private final UiThreadContext mUiThreadContext; private final Executor mBackgroundExecutor; private final UserTracker mUserTracker; private final NavigationModeController mNavigationModeController; @@ -1327,29 +1327,27 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack @Inject public Factory(OverviewProxyService overviewProxyService, - SysUiState sysUiState, - PluginManager pluginManager, - @Main Executor executor, - @Main Handler handler, - @Background Executor backgroundExecutor, - UserTracker userTracker, - NavigationModeController navigationModeController, - BackPanelController.Factory backPanelControllerFactory, - ViewConfiguration viewConfiguration, - WindowManager windowManager, - IWindowManager windowManagerService, - InputManager inputManager, - Optional<Pip> pipOptional, - Optional<DesktopMode> desktopModeOptional, - FalsingManager falsingManager, - Provider<BackGestureTfClassifierProvider> - backGestureTfClassifierProviderProvider, - Provider<LightBarController> lightBarControllerProvider) { + SysUiState sysUiState, + PluginManager pluginManager, + @BackPanelUiThread UiThreadContext uiThreadContext, + @Background Executor backgroundExecutor, + UserTracker userTracker, + NavigationModeController navigationModeController, + BackPanelController.Factory backPanelControllerFactory, + ViewConfiguration viewConfiguration, + WindowManager windowManager, + IWindowManager windowManagerService, + InputManager inputManager, + Optional<Pip> pipOptional, + Optional<DesktopMode> desktopModeOptional, + FalsingManager falsingManager, + Provider<BackGestureTfClassifierProvider> + backGestureTfClassifierProviderProvider, + Provider<LightBarController> lightBarControllerProvider) { mOverviewProxyService = overviewProxyService; mSysUiState = sysUiState; mPluginManager = pluginManager; - mExecutor = executor; - mHandler = handler; + mUiThreadContext = uiThreadContext; mBackgroundExecutor = backgroundExecutor; mUserTracker = userTracker; mNavigationModeController = navigationModeController; @@ -1367,26 +1365,26 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack /** Construct a {@link EdgeBackGestureHandler}. */ public EdgeBackGestureHandler create(Context context) { - return new EdgeBackGestureHandler( - context, - mOverviewProxyService, - mSysUiState, - mPluginManager, - mExecutor, - mHandler, - mBackgroundExecutor, - mUserTracker, - mNavigationModeController, - mBackPanelControllerFactory, - mViewConfiguration, - mWindowManager, - mWindowManagerService, - mInputManager, - mPipOptional, - mDesktopModeOptional, - mFalsingManager, - mBackGestureTfClassifierProviderProvider, - mLightBarControllerProvider); + return mUiThreadContext.runWithScissors( + () -> new EdgeBackGestureHandler( + context, + mOverviewProxyService, + mSysUiState, + mPluginManager, + mUiThreadContext, + mBackgroundExecutor, + mUserTracker, + mNavigationModeController, + mBackPanelControllerFactory, + mViewConfiguration, + mWindowManager, + mWindowManagerService, + mInputManager, + mPipOptional, + mDesktopModeOptional, + mFalsingManager, + mBackGestureTfClassifierProviderProvider, + mLightBarControllerProvider)); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java index d00916a1c1a8..c742f6413022 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java @@ -610,7 +610,7 @@ public final class KeyboardShortcuts { keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo( mContext.getString(R.string.keyboard_shortcut_group_applications_calendar), calendarIcon, - KeyEvent.KEYCODE_L, + KeyEvent.KEYCODE_K, KeyEvent.META_META_ON)); } 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 43258972ea34..1449e535c279 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 @@ -23,6 +23,7 @@ import android.telephony.satellite.NtnSignalStrengthCallback import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS import android.telephony.satellite.SatelliteModemStateCallback +import android.telephony.satellite.SatelliteSupportedStateCallback import androidx.annotation.VisibleForTesting import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -35,6 +36,7 @@ import com.android.systemui.log.core.MessagePrinter import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported @@ -162,60 +164,6 @@ constructor( @get:VisibleForTesting val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown) - init { - satelliteManager = satelliteManagerOpt.getOrNull() - - isSatelliteAllowedForCurrentLocation = MutableStateFlow(false) - - if (satelliteManager != null) { - // First, check that satellite is supported on this device - scope.launch { - val waitTime = ensureMinUptime(systemClock, MIN_UPTIME) - if (waitTime > 0) { - logBuffer.i({ long1 = waitTime }) { - "Waiting $long1 ms before checking for satellite support" - } - delay(waitTime) - } - - satelliteSupport.value = satelliteManager.checkSatelliteSupported() - - logBuffer.i( - { str1 = satelliteSupport.value.toString() }, - { "Checked for system support. support=$str1" }, - ) - - // We only need to check location availability if this mode is supported - if (satelliteSupport.value is Supported) { - isSatelliteAllowedForCurrentLocation.subscriptionCount - .map { it > 0 } - .distinctUntilChanged() - .collectLatest { hasSubscribers -> - if (hasSubscribers) { - /* - * As there is no listener available for checking satellite allowed, - * we must poll. Defaulting to polling at most once every hour while - * active. Subsequent OOS events will restart the job, so a flaky - * connection might cause more frequent checks. - */ - while (true) { - logBuffer.i { - "requestIsCommunicationAllowedForCurrentLocation" - } - checkIsSatelliteAllowed() - delay(POLLING_INTERVAL_MS) - } - } - } - } - } - } else { - logBuffer.i { "Satellite manager is null" } - - satelliteSupport.value = NotSupported - } - } - /** * Note that we are given an "unbound" [TelephonyManager] (meaning it was not created with a * specific `subscriptionId`). Therefore this is the radio power state of the @@ -269,6 +217,134 @@ constructor( } .onStart { emit(Unit) } + init { + satelliteManager = satelliteManagerOpt.getOrNull() + + isSatelliteAllowedForCurrentLocation = MutableStateFlow(false) + + if (satelliteManager != null) { + // Outer scope launch allows us to delay until MIN_UPTIME + scope.launch { + // First, check that satellite is supported on this device + satelliteSupport.value = checkSatelliteSupportAfterMinUptime(satelliteManager) + logBuffer.i( + { str1 = satelliteSupport.value.toString() }, + { "Checked for system support. support=$str1" }, + ) + + // Second, launch a job to poll for service availability based on location + scope.launch { pollForAvailabilityBasedOnLocation() } + + // Third, register a listener to let us know if there are changes to support + scope.launch { listenForChangesToSatelliteSupport(satelliteManager) } + } + } else { + logBuffer.i { "Satellite manager is null" } + satelliteSupport.value = NotSupported + } + } + + private suspend fun checkSatelliteSupportAfterMinUptime( + sm: SatelliteManager + ): SatelliteSupport { + val waitTime = ensureMinUptime(systemClock, MIN_UPTIME) + if (waitTime > 0) { + logBuffer.i({ long1 = waitTime }) { + "Waiting $long1 ms before checking for satellite support" + } + delay(waitTime) + } + + return sm.checkSatelliteSupported() + } + + /* + * As there is no listener available for checking satellite allowed, we must poll the service. + * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart + * the job, so a flaky connection might cause more frequent checks. + */ + private suspend fun pollForAvailabilityBasedOnLocation() { + satelliteSupport + .whenSupported( + supported = ::isSatelliteAllowedHasListener, + orElse = flowOf(false), + retrySignal = telephonyProcessCrashedEvent, + ) + .collectLatest { hasSubscribers -> + if (hasSubscribers) { + while (true) { + logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" } + checkIsSatelliteAllowed() + delay(POLLING_INTERVAL_MS) + } + } + } + } + + /** + * Register a callback with [SatelliteManager] to let us know if there is a change in satellite + * support. This job restarts if there is a crash event detected. + * + * Note that the structure of this method looks similar to [whenSupported], but since we want + * this callback registered even when it is [NotSupported], we just mimic the structure here. + */ + private suspend fun listenForChangesToSatelliteSupport(sm: SatelliteManager) { + telephonyProcessCrashedEvent.collectLatest { + satelliteIsSupportedCallback.collect { supported -> + if (supported) { + satelliteSupport.value = Supported(sm) + } else { + satelliteSupport.value = NotSupported + } + } + } + } + + /** + * Callback version of [checkSatelliteSupported]. This flow should be retried on the same + * [telephonyProcessCrashedEvent] signal, but does not require a [SupportedSatelliteManager], + * since it specifically watches for satellite support. + */ + private val satelliteIsSupportedCallback: Flow<Boolean> = + if (satelliteManager == null) { + flowOf(false) + } else { + conflatedCallbackFlow { + val callback = SatelliteSupportedStateCallback { supported -> + logBuffer.i { + "onSatelliteSupportedStateChanged: " + + "${if (supported) "supported" else "not supported"}" + } + trySend(supported) + } + + var registered = false + try { + satelliteManager.registerForSupportedStateChanged( + bgDispatcher.asExecutor(), + callback + ) + registered = true + } catch (e: Exception) { + logBuffer.e("error registering for supported state change", e) + } + + awaitClose { + if (registered) { + satelliteManager.unregisterForSupportedStateChanged(callback) + } + } + } + } + + /** + * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there + * are active listeners to [isSatelliteAllowedForCurrentLocation] + */ + @SuppressWarnings("unused") + private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> = + isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged() + override val connectionState = satelliteSupport .whenSupported( diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt index 83e3428bb95f..a7abb6b5f1d3 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt @@ -19,6 +19,7 @@ import android.os.Handler import android.os.HandlerThread import android.os.Looper import android.os.Process +import android.view.Choreographer import com.android.systemui.Dependency import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton @@ -31,6 +32,12 @@ import dagger.Module import dagger.Provides import java.util.concurrent.Executor import javax.inject.Named +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class BackPanelUiThread /** Dagger Module for classes found within the concurrent package. */ @Module @@ -106,6 +113,39 @@ object SysUIConcurrencyModule { return looper } + @Provides + @SysUISingleton + @BackPanelUiThread + fun provideBackPanelUiThreadContext( + @Main mainLooper: Looper, + @Main mainHandler: Handler, + @Main mainExecutor: Executor + ): UiThreadContext { + return if (Flags.edgeBackGestureHandlerThread()) { + val thread = + HandlerThread("BackPanelUiThread", Process.THREAD_PRIORITY_DISPLAY).apply { + start() + looper.setSlowLogThresholdMs( + LONG_SLOW_DISPATCH_THRESHOLD, + LONG_SLOW_DELIVERY_THRESHOLD + ) + } + UiThreadContext( + thread.looper, + thread.threadHandler, + thread.threadExecutor, + thread.threadHandler.runWithScissors { Choreographer.getInstance() } + ) + } else { + UiThreadContext( + mainLooper, + mainHandler, + mainExecutor, + mainHandler.runWithScissors { Choreographer.getInstance() } + ) + } + } + /** * Background Handler. * diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt new file mode 100644 index 000000000000..8c8c686dddcc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency + +import android.os.Handler +import android.os.Looper +import android.view.Choreographer +import com.android.systemui.util.Assert +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicReference + +private const val DEFAULT_TIMEOUT = 150L + +class UiThreadContext( + val looper: Looper, + val handler: Handler, + val executor: Executor, + val choreographer: Choreographer +) { + fun isCurrentThread() { + Assert.isCurrentThread(looper) + } + + fun <T> runWithScissors(block: () -> T): T { + return handler.runWithScissors(block) + } + + fun runWithScissors(block: Runnable) { + handler.runWithScissors(block, DEFAULT_TIMEOUT) + } +} + +fun <T> Handler.runWithScissors(block: () -> T): T { + val returnedValue = AtomicReference<T>() + runWithScissors({ returnedValue.set(block()) }, DEFAULT_TIMEOUT) + return returnedValue.get()!! +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt index c6b0dc542087..e0f64b4f7dc2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt @@ -18,8 +18,6 @@ package com.android.systemui.volume.domain.interactor import android.bluetooth.BluetoothAdapter import android.media.AudioDeviceInfo -import android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES -import android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.media.BluetoothMediaDevice @@ -81,30 +79,32 @@ constructor( val isInAudioSharing: Flow<Boolean> = audioSharingRepository.inAudioSharing private fun AudioDeviceInfo.toAudioOutputDevice(): AudioOutputDevice { - if (type == TYPE_WIRED_HEADPHONES || type == TYPE_WIRED_HEADSET) { - return AudioOutputDevice.Wired( - name = productName.toString(), - icon = deviceIconInteractor.loadIcon(type), - ) - } - val cachedBluetoothDevice: CachedBluetoothDevice? = - if (address.isEmpty() || localBluetoothManager == null || bluetoothAdapter == null) { - null - } else { - val remoteDevice = bluetoothAdapter.getRemoteDevice(address) - localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice) + if ( + BluetoothAdapter.checkBluetoothAddress(address) && + localBluetoothManager != null && + bluetoothAdapter != null + ) { + val remoteDevice = bluetoothAdapter.getRemoteDevice(address) + localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice)?.let { + device: CachedBluetoothDevice -> + return AudioOutputDevice.Bluetooth( + name = device.name, + icon = deviceIconInteractor.loadIcon(device), + cachedBluetoothDevice = device, + ) } - return cachedBluetoothDevice?.let { - AudioOutputDevice.Bluetooth( - name = it.name, - icon = deviceIconInteractor.loadIcon(it), - cachedBluetoothDevice = it, - ) } - ?: AudioOutputDevice.BuiltIn( + // Built-in device has an empty address + if (address.isNotEmpty()) { + return AudioOutputDevice.Wired( name = productName.toString(), icon = deviceIconInteractor.loadIcon(type), ) + } + return AudioOutputDevice.BuiltIn( + name = productName.toString(), + icon = deviceIconInteractor.loadIcon(type), + ) } private fun MediaDevice.toAudioOutputDevice(): AudioOutputDevice { @@ -115,7 +115,8 @@ constructor( icon = icon, cachedBluetoothDevice = cachedDevice, ) - deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE -> + deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE || + deviceType == MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE -> AudioOutputDevice.Wired( name = name, icon = icon, diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt index 99f956489bc3..3da725b9a51f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt @@ -98,12 +98,12 @@ constructor( private fun showNewVolumePanel() { activityStarter.dismissKeyguardThenExecute( - { + /* action = */ { volumePanelGlobalStateInteractor.setVisible(true) false }, - {}, - true + /* cancel = */ {}, + /* afterKeyguardGone = */ true, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 1167fce7524b..f46cfdc280fe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -383,11 +383,25 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } if (testCase.isFaceOnly) { - val expectedIconAsset = R.raw.face_dialog_authenticating + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark) + + val expectedIconAsset = + if (shouldPulseAnimation!!) { + if (lastPulseLightToDark!!) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + } else { + R.drawable.face_dialog_pulse_dark_to_light + } assertThat(iconAsset).isEqualTo(expectedIconAsset) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(true) } if (testCase.isCoex) { @@ -409,11 +423,26 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } } else { // implicit flow - val expectedIconAsset = R.raw.face_dialog_authenticating + val shouldRepeatAnimation by + collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark) + + val expectedIconAsset = + if (shouldPulseAnimation!!) { + if (lastPulseLightToDark!!) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + } else { + R.drawable.face_dialog_pulse_dark_to_light + } assertThat(iconAsset).isEqualTo(expectedIconAsset) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(true) } } } @@ -503,9 +532,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_error) assertThat(iconContentDescriptionId).isEqualTo(R.string.keyguard_face_failed) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) // Clear error, go to idle errorJob.join() @@ -514,6 +548,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_idle) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) } if (testCase.isCoex) { @@ -596,10 +631,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa // If co-ex, using implicit flow (explicit flow always requires confirmation) if (testCase.isFaceOnly || testCase.isCoex) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) } } } @@ -621,10 +661,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa ) if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_wink_from_dark) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) } // explicit flow because confirmation requested @@ -666,10 +711,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.confirmAuthenticated() if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) } // explicit flow because confirmation requested diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 3bf4173cd7c7..3906c40593f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -191,7 +191,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Before fun setup() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) staticMockSession = ExtendedMockito.mockitoSession() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt index e5d3082bb245..80ebe56453ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt @@ -376,7 +376,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) @@ -388,7 +388,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun attachPlayer_seekBarEnabled_seekBarVisible() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) @@ -399,7 +399,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun attachPlayer_seekBarStatusUpdate_seekBarVisibilityChanges() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) @@ -415,7 +415,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun attachPlayer_notScrubbing_scrubbingViewsGone() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.canShowScrubbingTime = true @@ -435,7 +435,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_noSemanticActions_scrubbingViewsGone() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.canShowScrubbingTime = false @@ -454,7 +454,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true) @@ -476,7 +476,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(false) @@ -498,7 +498,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true) @@ -524,7 +524,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_trueThenFalse_reservePrevAndNextButtons() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true, ConstraintSet.INVISIBLE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 02f53b6846e8..d24d87c6f57a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -34,6 +34,7 @@ import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAI import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN import android.telephony.satellite.SatelliteManager.SatelliteException import android.telephony.satellite.SatelliteModemStateCallback +import android.telephony.satellite.SatelliteSupportedStateCallback import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -327,7 +328,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { @Test fun satelliteNotSupported_listenersAreNotRegistered() = testScope.runTest { - setupDefaultRepo() // GIVEN satellite is not supported setUpRepo( uptime = MIN_UPTIME, @@ -345,6 +345,110 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { } @Test + fun satelliteSupported_registersCallbackForStateChanges() = + testScope.runTest { + // GIVEN a supported satellite manager. + setupDefaultRepo() + runCurrent() + + // THEN the repo registers for state changes of satellite support + verify(satelliteManager, times(1)).registerForSupportedStateChanged(any(), any()) + } + + @Test + fun satelliteNotSupported_registersCallbackForStateChanges() = + testScope.runTest { + // GIVEN satellite is not supported + setUpRepo( + uptime = MIN_UPTIME, + satMan = satelliteManager, + satelliteSupported = false, + ) + + runCurrent() + // THEN the repo registers for state changes of satellite support + verify(satelliteManager, times(1)).registerForSupportedStateChanged(any(), any()) + } + + @Test + fun satelliteSupportedStateChangedCallbackThrows_doesNotCrash() = + testScope.runTest { + // GIVEN, satellite manager throws when registering for supported state changes + whenever(satelliteManager.registerForSupportedStateChanged(any(), any())) + .thenThrow(IllegalStateException()) + + // GIVEN a supported satellite manager. + setupDefaultRepo() + runCurrent() + + // THEN a listener for satellite supported changed can attempt to register, + // with no crash + verify(satelliteManager).registerForSupportedStateChanged(any(), any()) + } + + @Test + fun satelliteSupported_supportIsLost_unregistersListeners() = + testScope.runTest { + // GIVEN a supported satellite manager. + setupDefaultRepo() + runCurrent() + + val callback = + withArgCaptor<SatelliteSupportedStateCallback> { + verify(satelliteManager).registerForSupportedStateChanged(any(), capture()) + } + + // WHEN data is requested from the repo + val connectionState by collectLastValue(underTest.connectionState) + val signalStrength by collectLastValue(underTest.signalStrength) + + // THEN the listeners are registered + verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any()) + verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any()) + + // WHEN satellite support turns off + callback.onSatelliteSupportedStateChanged(false) + runCurrent() + + // THEN listeners are unregistered + verify(satelliteManager, times(1)).unregisterForModemStateChanged(any()) + verify(satelliteManager, times(1)).unregisterForNtnSignalStrengthChanged(any()) + } + + @Test + fun satelliteNotSupported_supportShowsUp_registersListeners() = + testScope.runTest { + // GIVEN satellite is not supported + setUpRepo( + uptime = MIN_UPTIME, + satMan = satelliteManager, + satelliteSupported = false, + ) + runCurrent() + + val callback = + withArgCaptor<SatelliteSupportedStateCallback> { + verify(satelliteManager).registerForSupportedStateChanged(any(), capture()) + } + + // WHEN data is requested from the repo + val connectionState by collectLastValue(underTest.connectionState) + val signalStrength by collectLastValue(underTest.signalStrength) + + // THEN the listeners are not yet registered + verify(satelliteManager, times(0)).registerForModemStateChanged(any(), any()) + verify(satelliteManager, times(0)).registerForNtnSignalStrengthChanged(any(), any()) + + // WHEN satellite support turns on + callback.onSatelliteSupportedStateChanged(true) + runCurrent() + + // THEN listeners are registered + verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any()) + verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any()) + } + + @Test fun repoDoesNotCheckForSupportUntilMinUptime() = testScope.runTest { // GIVEN we init 100ms after sysui starts up diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt index 3ac712918100..15ef26d58ece 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt @@ -33,19 +33,22 @@ object TestAudioDevicesFactory { ) } - fun wiredDevice(deviceName: String = "wired"): AudioDeviceInfo { + fun wiredDevice( + deviceName: String = "wired", + deviceAddress: String = "card=1;device=0", + ): AudioDeviceInfo { return AudioDeviceInfo( AudioDevicePort.createForTesting( AudioDeviceInfo.TYPE_WIRED_HEADPHONES, deviceName, - "", + deviceAddress, ) ) } fun bluetoothDevice( deviceName: String = "bt", - deviceAddress: String = "test_address", + deviceAddress: String = "00:43:A8:23:10:F0", ): AudioDeviceInfo { return AudioDeviceInfo( AudioDevicePort.createForTesting( diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 42f168bb4a6b..77decb6a52fa 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -1379,6 +1379,30 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } } + @RequiresNoPermission + @Override + public boolean isMagnificationSystemUIConnected() { + if (svcConnTracingEnabled()) { + logTraceSvcConn("isMagnificationSystemUIConnected", ""); + } + synchronized (mLock) { + if (!hasRightsToCurrentUserLocked()) { + return false; + } + if (!mSecurityPolicy.canControlMagnification(this)) { + return false; + } + final long identity = Binder.clearCallingIdentity(); + try { + MagnificationProcessor magnificationProcessor = + mSystemSupport.getMagnificationProcessor(); + return magnificationProcessor.isMagnificationSystemUIConnected(); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + public boolean isMagnificationCallbackEnabled(int displayId) { return mInvocationHandler.isMagnificationCallbackEnabled(displayId); } @@ -1925,6 +1949,11 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ InvocationHandler.MSG_CLEAR_ACCESSIBILITY_CACHE); } + public void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) { + mInvocationHandler + .notifyMagnificationSystemUIConnectionChangedLocked(connected); + } + public void notifyMagnificationChangedLocked(int displayId, @NonNull Region region, @NonNull MagnificationConfig config) { mInvocationHandler @@ -1976,6 +2005,21 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ return (mGenericMotionEventSources & eventSourceWithoutClass) != 0; } + /** + * Called by the invocation handler to notify the service that the + * magnification systemui connection has changed. + */ + private void notifyMagnificationSystemUIConnectionChangedInternal(boolean connected) { + final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); + if (listener != null) { + try { + listener.onMagnificationSystemUIConnectionChanged(connected); + } catch (RemoteException re) { + Slog.e(LOG_TAG, + "Error sending magnification sysui connection changes to " + mService, re); + } + } + } /** * Called by the invocation handler to notify the service that the @@ -2372,6 +2416,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private static final int MSG_BIND_INPUT = 12; private static final int MSG_UNBIND_INPUT = 13; private static final int MSG_START_INPUT = 14; + private static final int MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED = 15; /** List of magnification callback states, mapping from displayId -> Boolean */ @GuardedBy("mlock") @@ -2398,6 +2443,13 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ notifyClearAccessibilityCacheInternal(); } break; + case MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED: { + final SomeArgs args = (SomeArgs) message.obj; + final boolean connected = args.argi1 == 1; + notifyMagnificationSystemUIConnectionChangedInternal(connected); + args.recycle(); + } break; + case MSG_ON_MAGNIFICATION_CHANGED: { final SomeArgs args = (SomeArgs) message.obj; final Region region = (Region) args.arg1; @@ -2455,6 +2507,15 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } } + public void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) { + final SomeArgs args = SomeArgs.obtain(); + args.argi1 = connected ? 1 : 0; + + final Message msg = + obtainMessage(MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED, args); + msg.sendToTarget(); + } + public void notifyMagnificationChangedLocked(int displayId, @NonNull Region region, @NonNull MagnificationConfig config) { synchronized (mLock) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 20b727cd6f09..fe083389fa77 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1812,6 +1812,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** + * Called by the MagnificationController when the magnification systemui connection changes. + * + * @param connected Whether the connection is ready. + */ + public void notifyMagnificationSystemUIConnectionChanged(boolean connected) { + synchronized (mLock) { + notifyMagnificationSystemUIConnectionChangedLocked(connected); + } + } + + /** * Called by the MagnificationController when the state of display * magnification changes. * @@ -2243,6 +2254,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mProxyManager.clearCacheLocked(); } + private void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) { + final AccessibilityUserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + final AccessibilityServiceConnection service = state.mBoundServices.get(i); + service.notifyMagnificationSystemUIConnectionChangedLocked(connected); + } + } + private void notifyMagnificationChangedLocked(int displayId, @NonNull Region region, @NonNull MagnificationConfig config) { final AccessibilityUserState state = getCurrentUserStateLocked(); diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java index 4cb3d247edb0..420bac759ea6 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java @@ -489,6 +489,14 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon /** @throws UnsupportedOperationException since a proxy does not need magnification */ @RequiresNoPermission @Override + public boolean isMagnificationSystemUIConnected() throws UnsupportedOperationException { + throw new UnsupportedOperationException("isMagnificationSystemUIConnected is not" + + " supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @RequiresNoPermission + @Override public boolean isMagnificationCallbackEnabled(int displayId) { throw new UnsupportedOperationException("isMagnificationCallbackEnabled is not supported"); } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java index 0719ebaba707..7f4c808b2251 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java @@ -126,6 +126,7 @@ public class MagnificationConnectionManager implements @ConnectionState private int mConnectionState = DISCONNECTED; + ConnectionStateChangedCallback mConnectionStateChangedCallback = null; private static final int WAIT_CONNECTION_TIMEOUT_MILLIS = 100; @@ -264,6 +265,9 @@ public class MagnificationConnectionManager implements } } } + if (mConnectionStateChangedCallback != null) { + mConnectionStateChangedCallback.onConnectionStateChanged(connection != null); + } } /** @@ -271,7 +275,7 @@ public class MagnificationConnectionManager implements */ public boolean isConnected() { synchronized (mLock) { - return mConnectionWrapper != null; + return mConnectionWrapper != null && mConnectionState == CONNECTED; } } @@ -1344,4 +1348,8 @@ public class MagnificationConnectionManager implements } } } + + interface ConnectionStateChangedCallback { + void onConnectionStateChanged(boolean connected); + } } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index 76367a2b11c3..9b7884711a6d 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -828,6 +828,8 @@ public class MagnificationController implements MagnificationConnectionManager.C mMagnificationConnectionManager = new MagnificationConnectionManager(mContext, mLock, this, mAms.getTraceManager(), mScaleProvider); + mMagnificationConnectionManager.mConnectionStateChangedCallback = + mAms::notifyMagnificationSystemUIConnectionChanged; } return mMagnificationConnectionManager; } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java index ed8f1ab3a1b2..603683906d06 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java @@ -147,6 +147,10 @@ public class MagnificationProcessor { return false; } + public boolean isMagnificationSystemUIConnected() { + return mController.getMagnificationConnectionManager().isConnected(); + } + private boolean setScaleAndCenterForFullScreenMagnification(int displayId, float scale, float centerX, float centerY, boolean animate, int id) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 215f6402fa76..4a9900763a94 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -29,6 +29,7 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOAR import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery; +import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix; import android.annotation.EnforcePermission; import android.annotation.NonNull; @@ -1478,7 +1479,13 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub synchronized (mVirtualDeviceLock) { boolean hasInterceptedIntent = false; for (Map.Entry<IBinder, IntentFilter> interceptor : mIntentInterceptors.entrySet()) { - if (interceptor.getValue().match( + IntentFilter intentFilter = interceptor.getValue(); + // Explicitly match the actions because the intent filter will match any intent + // without an explicit action. If the intent has no action, then require that there + // are no actions specified in the filter either. + boolean explicitActionMatch = !intentInterceptionActionMatchingFix() + || intent.getAction() != null || intentFilter.countActions() == 0; + if (explicitActionMatch && intentFilter.match( intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(), intent.getCategories(), TAG) >= 0) { try { diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java index 966478e33c73..a61925732256 100644 --- a/services/core/java/com/android/server/PackageWatchdog.java +++ b/services/core/java/com/android/server/PackageWatchdog.java @@ -138,12 +138,6 @@ public class PackageWatchdog { static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10); - // Time needed to apply mitigation - private static final String MITIGATION_WINDOW_MS = - "persist.device_config.configuration.mitigation_window_ms"; - @VisibleForTesting - static final long DEFAULT_MITIGATION_WINDOW_MS = TimeUnit.SECONDS.toMillis(5); - // Threshold level at which or above user might experience significant disruption. private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD = "persist.device_config.configuration.major_user_impact_level_threshold"; @@ -216,9 +210,6 @@ public class PackageWatchdog { @GuardedBy("mLock") private boolean mSyncRequired = false; - @GuardedBy("mLock") - private long mLastMitigation = -1000000; - @FunctionalInterface @VisibleForTesting interface SystemClock { @@ -409,16 +400,6 @@ public class PackageWatchdog { Slog.w(TAG, "Could not resolve a list of failing packages"); return; } - synchronized (mLock) { - final long now = mSystemClock.uptimeMillis(); - if (Flags.recoverabilityDetection()) { - if (now >= mLastMitigation - && (now - mLastMitigation) < getMitigationWindowMs()) { - Slog.i(TAG, "Skipping onPackageFailure mitigation"); - return; - } - } - } mLongTaskHandler.post(() -> { synchronized (mLock) { if (mAllObservers.isEmpty()) { @@ -519,17 +500,10 @@ public class PackageWatchdog { int currentObserverImpact, int mitigationCount) { if (currentObserverImpact < getUserImpactLevelLimit()) { - synchronized (mLock) { - mLastMitigation = mSystemClock.uptimeMillis(); - } currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount); } } - private long getMitigationWindowMs() { - return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS); - } - /** * Called when the system server boots. If the system server is detected to be in a boot loop, diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 8d7a1c9f8228..8eef71e603b2 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -22,6 +22,8 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; +import static android.os.Process.ROOT_UID; +import static android.os.Process.SYSTEM_UID; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -422,6 +424,10 @@ public final class PendingIntentRecord extends IIntentSender.Stub { }) public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges( int callingUid, @Nullable String callingPackage) { + if (callingUid == ROOT_UID || callingUid == SYSTEM_UID) { + // root and system must always opt in explicitly + return BackgroundStartPrivileges.ALLOW_FGS; + } boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled( DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage, UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled( diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java index 02e80d611f3f..f652b33b3fd3 100644 --- a/services/core/java/com/android/server/audio/AudioPolicyFacade.java +++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java @@ -16,12 +16,14 @@ package com.android.server.audio; +import com.android.media.permission.INativePermissionController; /** * Facade to IAudioPolicyService which fulfills AudioService dependencies. * See @link{IAudioPolicyService.aidl} */ public interface AudioPolicyFacade { - public boolean isHotwordStreamSupported(boolean lookbackAudio); + public INativePermissionController getPermissionController(); + public void registerOnStartTask(Runnable r); } diff --git a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java new file mode 100644 index 000000000000..5ea3c4bf538d --- /dev/null +++ b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java @@ -0,0 +1,149 @@ +/* + * 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.audio; + +import android.annotation.Nullable; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.ArraySet; + +import com.android.internal.annotations.GuardedBy; +import com.android.media.permission.INativePermissionController; +import com.android.media.permission.UidPackageState; +import com.android.server.pm.pkg.PackageState; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** Responsible for synchronizing system server permission state to the native audioserver. */ +public class AudioServerPermissionProvider { + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private INativePermissionController mDest; + + @GuardedBy("mLock") + private final Map<Integer, Set<String>> mPackageMap; + + /** + * @param appInfos - PackageState for all apps on the device, used to populate init state + */ + public AudioServerPermissionProvider(Collection<PackageState> appInfos) { + // Initialize the package state + mPackageMap = generatePackageMappings(appInfos); + } + + /** + * Called whenever audioserver starts (or started before us) + * + * @param pc - The permission controller interface from audioserver, which we push updates to + */ + public void onServiceStart(@Nullable INativePermissionController pc) { + if (pc == null) return; + synchronized (mLock) { + mDest = pc; + resetNativePackageState(); + } + } + + /** + * Called when a package is added or removed + * + * @param uid - uid of modified package (only app-id matters) + * @param packageName - the (new) packageName + * @param isRemove - true if the package is being removed, false if it is being added + */ + public void onModifyPackageState(int uid, String packageName, boolean isRemove) { + // No point in maintaining package mappings for uids of different users + uid = UserHandle.getAppId(uid); + synchronized (mLock) { + // Update state + Set<String> packages; + if (!isRemove) { + packages = mPackageMap.computeIfAbsent(uid, unused -> new ArraySet(1)); + packages.add(packageName); + } else { + packages = mPackageMap.get(uid); + if (packages != null) { + packages.remove(packageName); + if (packages.isEmpty()) mPackageMap.remove(uid); + } + } + // Push state to destination + if (mDest == null) { + return; + } + var state = new UidPackageState(); + state.uid = uid; + state.packageNames = packages != null ? List.copyOf(packages) : Collections.emptyList(); + try { + mDest.updatePackagesForUid(state); + } catch (RemoteException e) { + // We will re-init the state when the service comes back up + mDest = null; + } + } + } + + /** Called when full syncing package state to audioserver. */ + @GuardedBy("mLock") + private void resetNativePackageState() { + if (mDest == null) return; + List<UidPackageState> states = + mPackageMap.entrySet().stream() + .map( + entry -> { + UidPackageState state = new UidPackageState(); + state.uid = entry.getKey(); + state.packageNames = List.copyOf(entry.getValue()); + return state; + }) + .toList(); + try { + mDest.populatePackagesForUids(states); + } catch (RemoteException e) { + // We will re-init the state when the service comes back up + mDest = null; + } + } + + /** + * Aggregation operation on all package states list: groups by states by app-id and merges the + * packageName for each state into an ArraySet. + */ + private static Map<Integer, Set<String>> generatePackageMappings( + Collection<PackageState> appInfos) { + Collector<PackageState, Object, Set<String>> reducer = + Collectors.mapping( + (PackageState p) -> p.getPackageName(), + Collectors.toCollection(() -> new ArraySet(1))); + + return appInfos.stream() + .collect( + Collectors.groupingBy( + /* predicate */ (PackageState p) -> p.getAppId(), + /* factory */ HashMap::new, + /* downstream collector */ reducer)); + } +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 2a23b9ca522e..ef65b2523024 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -31,6 +31,10 @@ import static android.Manifest.permission.MODIFY_PHONE_STATE; import static android.Manifest.permission.QUERY_AUDIO_STATE; import static android.Manifest.permission.WRITE_SETTINGS; import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.EXTRA_ARCHIVAL; +import static android.content.Intent.EXTRA_REPLACING; import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET; import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER; import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; @@ -57,7 +61,9 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_OFF; import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.media.audio.Flags.absVolumeIndexFix; import static com.android.media.audio.Flags.alarmMinVolumeZero; +import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; import static com.android.media.audio.Flags.setStreamVolumeOrder; @@ -239,15 +245,18 @@ import com.android.internal.os.SomeArgs; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; +import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent; import com.android.server.audio.AudioServiceEvents.PhoneStateEvent; import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent; import com.android.server.audio.AudioServiceEvents.VolumeEvent; +import com.android.server.pm.PackageManagerLocal; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerInternal.UserRestrictionsListener; import com.android.server.pm.UserManagerService; +import com.android.server.pm.pkg.PackageState; import com.android.server.utils.EventLogger; import com.android.server.wm.ActivityTaskManagerInternal; @@ -272,6 +281,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; @@ -302,6 +312,8 @@ public class AudioService extends IAudioService.Stub private final SettingsAdapter mSettings; private final AudioPolicyFacade mAudioPolicy; + private final AudioServerPermissionProvider mPermissionProvider; + private final MusicFxHelper mMusicFxHelper; /** Debug audio mode */ @@ -632,6 +644,17 @@ public class AudioService extends IAudioService.Stub // If absolute volume is supported in AVRCP device private volatile boolean mAvrcpAbsVolSupported = false; + private final Object mCachedAbsVolDrivingStreamsLock = new Object(); + // Contains for all the device types which support absolute volume the current streams that + // are driving the volume changes + @GuardedBy("mCachedAbsVolDrivingStreamsLock") + private final HashMap<Integer, Integer> mCachedAbsVolDrivingStreams = new HashMap<>( + Map.of(AudioSystem.DEVICE_OUT_BLE_HEADSET, AudioSystem.STREAM_MUSIC, + AudioSystem.DEVICE_OUT_BLE_SPEAKER, AudioSystem.STREAM_MUSIC, + AudioSystem.DEVICE_OUT_BLE_BROADCAST, AudioSystem.STREAM_MUSIC, + AudioSystem.DEVICE_OUT_HEARING_AID, AudioSystem.STREAM_MUSIC + )); + /** * Default stream type used for volume control in the absence of playback * e.g. user on homescreen, no app playing anything, presses hardware volume buttons, this @@ -1009,14 +1032,22 @@ public class AudioService extends IAudioService.Stub public Lifecycle(Context context) { super(context); + var audioserverLifecycleExecutor = Executors.newSingleThreadExecutor(); + var audioPolicyFacade = new DefaultAudioPolicyFacade(audioserverLifecycleExecutor); mService = new AudioService(context, AudioSystemAdapter.getDefaultAdapter(), SystemServerAdapter.getDefaultAdapter(context), SettingsAdapter.getDefaultAdapter(), new AudioVolumeGroupHelper(), - new DefaultAudioPolicyFacade(r -> r.run()), - null); - + audioPolicyFacade, + null, + context.getSystemService(AppOpsManager.class), + PermissionEnforcer.fromContext(context), + audioserverPermissions() ? + initializeAudioServerPermissionProvider( + context, audioPolicyFacade, audioserverLifecycleExecutor) : + null + ); } @Override @@ -1093,25 +1124,6 @@ public class AudioService extends IAudioService.Stub /** * @param context * @param audioSystem Adapter for {@link AudioSystem} - * @param systemServer Adapter for privileged functionality for system server components - * @param settings Adapter for {@link Settings} - * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup} - * @param audioPolicy Interface of a facade to IAudioPolicyManager - * @param looper Looper to use for the service's message handler. If this is null, an - * {@link AudioSystemThread} is created as the messaging thread instead. - */ - public AudioService(Context context, AudioSystemAdapter audioSystem, - SystemServerAdapter systemServer, SettingsAdapter settings, - AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, - @Nullable Looper looper) { - this (context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, context.getSystemService(AppOpsManager.class), - PermissionEnforcer.fromContext(context)); - } - - /** - * @param context - * @param audioSystem Adapter for {@link AudioSystem} * @param systemServer Adapter for privilieged functionality for system server components * @param settings Adapter for {@link Settings} * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup} @@ -1125,13 +1137,16 @@ public class AudioService extends IAudioService.Stub public AudioService(Context context, AudioSystemAdapter audioSystem, SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, - @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer) { + @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer, + /* @NonNull */ AudioServerPermissionProvider permissionProvider) { super(enforcer); sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()")); mContext = context; mContentResolver = context.getContentResolver(); mAppOps = appOps; + mPermissionProvider = permissionProvider; + mAudioSystem = audioSystem; mSystemServer = systemServer; mAudioVolumeGroupHelper = audioVolumeGroupHelper; @@ -1472,6 +1487,13 @@ public class AudioService extends IAudioService.Stub // check on volume initialization checkVolumeRangeInitialization("AudioService()"); + + synchronized (mCachedAbsVolDrivingStreamsLock) { + mCachedAbsVolDrivingStreams.forEach((dev, stream) -> { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true, + stream); + }); + } } private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener = @@ -1911,6 +1933,14 @@ public class AudioService extends IAudioService.Stub } onIndicateSystemReady(); + + synchronized (mCachedAbsVolDrivingStreamsLock) { + mCachedAbsVolDrivingStreams.forEach((dev, stream) -> { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true, + stream); + }); + } + // indicate the end of reconfiguration phase to audio HAL AudioSystem.setParameters("restarting=false"); @@ -3737,8 +3767,10 @@ public class AudioService extends IAudioService.Stub int newIndex = mStreamStates[streamType].getIndex(device); + int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() : + AudioSystem.STREAM_MUSIC; // Check if volume update should be send to AVRCP - if (streamTypeAlias == AudioSystem.STREAM_MUSIC + if (streamTypeAlias == streamToDriveAbsVol && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { if (DEBUG_VOL) { @@ -4530,6 +4562,8 @@ public class AudioService extends IAudioService.Stub + featureSpatialAudioHeadtrackingLowLatency()); pw.println("\tandroid.media.audio.focusFreezeTestApi:" + focusFreezeTestApi()); + pw.println("\tcom.android.media.audio.audioserverPermissions:" + + audioserverPermissions()); pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:" + disablePrescaleAbsoluteVolume()); pw.println("\tcom.android.media.audio.setStreamVolumeOrder:" @@ -4540,6 +4574,8 @@ public class AudioService extends IAudioService.Stub + scoManagedByAudio()); pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:" + vgsVssSyncMuteOrder()); + pw.println("\tcom.android.media.audio.absVolumeIndexFix:" + + absVolumeIndexFix()); } private void dumpAudioMode(PrintWriter pw) { @@ -4735,7 +4771,9 @@ public class AudioService extends IAudioService.Stub } } - if (streamTypeAlias == AudioSystem.STREAM_MUSIC + int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() : + AudioSystem.STREAM_MUSIC; + if (streamTypeAlias == streamToDriveAbsVol && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { if (DEBUG_VOL) { @@ -6184,6 +6222,17 @@ public class AudioService extends IAudioService.Stub setLeAudioVolumeOnModeUpdate(mode, device, streamAlias, index, maxIndex); + synchronized (mCachedAbsVolDrivingStreamsLock) { + mCachedAbsVolDrivingStreams.replaceAll((absDev, stream) -> { + int streamToDriveAbs = getBluetoothContextualVolumeStream(); + if (stream != streamToDriveAbs) { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(absDev, /*address=*/ + "", /*enabled*/true, streamToDriveAbs); + } + return streamToDriveAbs; + }); + } + // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO // connections not started by the application changing the mode when pid changes mDeviceBroker.postSetModeOwner(mode, pid, uid); @@ -8105,6 +8154,10 @@ public class AudioService extends IAudioService.Stub return mAudioVolumeGroup.name(); } + public int getId() { + return mAudioVolumeGroup.getId(); + } + /** * Volume group with non null minimum index are considered as non mutable, thus * bijectivity is broken with potential associated stream type. @@ -8755,24 +8808,30 @@ public class AudioService extends IAudioService.Stub } private int getAbsoluteVolumeIndex(int index) { - /* Special handling for Bluetooth Absolute Volume scenario - * If we send full audio gain, some accessories are too loud even at its lowest - * volume. We are not able to enumerate all such accessories, so here is the - * workaround from phone side. - * Pre-scale volume at lowest volume steps 1 2 and 3. - * For volume step 0, set audio gain to 0 as some accessories won't mute on their end. - */ - if (index == 0) { - // 0% for volume 0 - index = 0; - } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) { - // Pre-scale for volume steps 1 2 and 3 - index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10; + if (absVolumeIndexFix()) { + // The attenuation is applied in the APM. No need to manipulate the index here + return index; } else { - // otherwise, full gain - index = (mIndexMax + 5) / 10; + /* Special handling for Bluetooth Absolute Volume scenario + * If we send full audio gain, some accessories are too loud even at its lowest + * volume. We are not able to enumerate all such accessories, so here is the + * workaround from phone side. + * Pre-scale volume at lowest volume steps 1 2 and 3. + * For volume step 0, set audio gain to 0 as some accessories won't mute on their + * end. + */ + if (index == 0) { + // 0% for volume 0 + index = 0; + } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) { + // Pre-scale for volume steps 1 2 and 3 + index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10; + } else { + // otherwise, full gain + index = (mIndexMax + 5) / 10; + } + return index; } - return index; } private void setStreamVolumeIndex(int index, int device) { @@ -8783,6 +8842,11 @@ public class AudioService extends IAudioService.Stub && !isFullyMuted()) { index = 1; } + + if (DEBUG_VOL) { + Log.d(TAG, "setStreamVolumeIndexAS(" + mStreamType + ", " + index + ", " + device + + ")"); + } mAudioSystem.setStreamVolumeIndexAS(mStreamType, index, device); } @@ -8794,14 +8858,24 @@ public class AudioService extends IAudioService.Stub } else if (isAbsoluteVolumeDevice(device) || isA2dpAbsoluteVolumeDevice(device) || AudioSystem.isLeAudioDeviceType(device)) { - index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10); + // do not change the volume logic for dynamic abs behavior devices like HDMI + if (absVolumeIndexFix() && isAbsoluteVolumeDevice(device)) { + index = getAbsoluteVolumeIndex((mIndexMax + 5) / 10); + } else { + index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10); + } } else if (isFullVolumeDevice(device)) { index = (mIndexMax + 5)/10; } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) { - index = (mIndexMax + 5)/10; + if (absVolumeIndexFix()) { + index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10); + } else { + index = (mIndexMax + 5) / 10; + } } else { index = (getIndex(device) + 5)/10; } + setStreamVolumeIndex(index, device); } @@ -8819,11 +8893,22 @@ public class AudioService extends IAudioService.Stub || isA2dpAbsoluteVolumeDevice(device) || AudioSystem.isLeAudioDeviceType(device)) { isAbsoluteVolume = true; - index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10); + // do not change the volume logic for dynamic abs behavior devices + // like HDMI + if (absVolumeIndexFix() && isAbsoluteVolumeDevice(device)) { + index = getAbsoluteVolumeIndex((mIndexMax + 5) / 10); + } else { + index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10); + } } else if (isFullVolumeDevice(device)) { index = (mIndexMax + 5)/10; } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) { - index = (mIndexMax + 5)/10; + if (absVolumeIndexFix()) { + isAbsoluteVolume = true; + index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10); + } else { + index = (mIndexMax + 5) / 10; + } } else { index = (mIndexMap.valueAt(i) + 5)/10; } @@ -9820,6 +9905,27 @@ public class AudioService extends IAudioService.Stub /*package*/ void setAvrcpAbsoluteVolumeSupported(boolean support) { mAvrcpAbsVolSupported = support; + if (absVolumeIndexFix()) { + int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP; + synchronized (mCachedAbsVolDrivingStreamsLock) { + mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> { + if (stream != null && !mAvrcpAbsVolSupported) { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/ + "", /*enabled*/false, AudioSystem.DEVICE_NONE); + return null; + } + // For A2DP and AVRCP we need to set the driving stream based on the + // BT contextual stream. Hence, we need to make sure in adjustStreamVolume + // and setStreamVolume that the driving abs volume stream is consistent. + int streamToDriveAbs = getBluetoothContextualVolumeStream(); + if (stream == null || stream != streamToDriveAbs) { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/ + "", /*enabled*/true, streamToDriveAbs); + } + return streamToDriveAbs; + }); + } + } sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0, mStreamStates[AudioSystem.STREAM_MUSIC], 0); @@ -11836,6 +11942,45 @@ public class AudioService extends IAudioService.Stub private static final String mMetricsId = MediaMetrics.Name.AUDIO_SERVICE + MediaMetrics.SEPARATOR; + private static AudioServerPermissionProvider initializeAudioServerPermissionProvider( + Context context, AudioPolicyFacade audioPolicy, Executor audioserverExecutor) { + Collection<PackageState> packageStates = null; + try (PackageManagerLocal.UnfilteredSnapshot snapshot = + LocalManagerRegistry.getManager(PackageManagerLocal.class) + .withUnfilteredSnapshot()) { + packageStates = snapshot.getPackageStates().values(); + } + var provider = new AudioServerPermissionProvider(packageStates); + audioPolicy.registerOnStartTask(() -> { + provider.onServiceStart(audioPolicy.getPermissionController()); + }); + + // Set up event listeners + IntentFilter packageUpdateFilter = new IntentFilter(); + packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED); + packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED); + packageUpdateFilter.addDataScheme("package"); + + context.registerReceiverForAllUsers(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + String pkgName = intent.getData().getEncodedSchemeSpecificPart(); + int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); + if (intent.getBooleanExtra(EXTRA_REPLACING, false) || + intent.getBooleanExtra(EXTRA_ARCHIVAL, false)) return; + if (action.equals(ACTION_PACKAGE_ADDED)) { + audioserverExecutor.execute(() -> + provider.onModifyPackageState(uid, pkgName, false /* isRemoved */)); + } else if (action.equals(ACTION_PACKAGE_REMOVED)) { + audioserverExecutor.execute(() -> + provider.onModifyPackageState(uid, pkgName, true /* isRemoved */)); + } + } + }, packageUpdateFilter, null, null); // main thread is fine, since dispatch on executor + return provider; + } + // Inform AudioFlinger of our device's low RAM attribute private static void readAndSetLowRamDevice() { diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index 7202fa286453..7f4bc74bd59e 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -598,6 +598,21 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } /** + * Same as {@link AudioSystem#setDeviceAbsoluteVolumeEnabled(int, String, boolean, int)} + * @param nativeDeviceType the internal device type for which absolute volume is + * enabled/disabled + * @param address the address of the device for which absolute volume is enabled/disabled + * @param enabled whether the absolute volume is enabled/disabled + * @param streamToDriveAbs the stream that is controlling the absolute volume + * @return status of indicating the success of this operation + */ + public int setDeviceAbsoluteVolumeEnabled(int nativeDeviceType, @NonNull String address, + boolean enabled, int streamToDriveAbs) { + return AudioSystem.setDeviceAbsoluteVolumeEnabled(nativeDeviceType, address, enabled, + streamToDriveAbs); + } + + /** * Same as {@link AudioSystem#registerPolicyMixes(ArrayList, boolean)} * @param mixes * @param register diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java index 75febbc1cf9c..09701e49a8ac 100644 --- a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java +++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java @@ -16,10 +16,15 @@ package com.android.server.audio; +import android.annotation.Nullable; import android.media.IAudioPolicyService; +import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import com.android.media.permission.INativePermissionController; + +import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Function; @@ -43,6 +48,7 @@ public class DefaultAudioPolicyFacade implements AudioPolicyFacade { (Function<IBinder, IAudioPolicyService>) IAudioPolicyService.Stub::asInterface, e); + mServiceHolder.registerOnStartTask(i -> Binder.allowBlocking(i.asBinder())); } @Override @@ -55,4 +61,23 @@ public class DefaultAudioPolicyFacade implements AudioPolicyFacade { throw new IllegalStateException(); } } + + @Override + public @Nullable INativePermissionController getPermissionController() { + IAudioPolicyService ap = mServiceHolder.checkService(); + if (ap == null) return null; + try { + var res = Objects.requireNonNull(ap.getPermissionController()); + Binder.allowBlocking(res.asBinder()); + return res; + } catch (RemoteException e) { + mServiceHolder.attemptClear(ap.asBinder()); + return null; + } + } + + @Override + public void registerOnStartTask(Runnable task) { + mServiceHolder.registerOnStartTask(unused -> task.run()); + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index dbd1e65e8b0f..6e027c6d44c4 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1029,6 +1029,10 @@ public class HdmiControlService extends SystemService { /** Helper method for sending feature discovery command */ private void reportFeatures(boolean isTvDeviceSetting) { + // <Report Features> should only be sent for HDMI 2.0 + if (getCecVersion() < HdmiControlManager.HDMI_CEC_VERSION_2_0) { + return; + } // check if tv device is enabled for tv device specific RC profile setting if (isTvDeviceSetting) { if (isTvDeviceEnabled()) { diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java index dd6433d98553..82ecb4acb197 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java @@ -16,12 +16,16 @@ package com.android.server.inputmethod; +import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.annotation.WorkerThread; import android.content.Context; import android.content.pm.UserInfo; import android.os.Handler; +import android.os.Process; +import android.util.IntArray; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -29,6 +33,10 @@ import com.android.internal.inputmethod.DirectBootAwareness; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; +import java.util.ArrayList; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + /** * Provides accesses to per-user additional {@link android.view.inputmethod.InputMethodSubtype} * persistent storages. @@ -38,6 +46,152 @@ final class AdditionalSubtypeMapRepository { @NonNull private static final SparseArray<AdditionalSubtypeMap> sPerUserMap = new SparseArray<>(); + record WriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap, + @NonNull InputMethodMap inputMethodMap) { + } + + static final class SingleThreadedBackgroundWriter { + /** + * A {@link ReentrantLock} used to guard {@link #mPendingTasks} and {@link #mRemovedUsers}. + */ + @NonNull + private final ReentrantLock mLock = new ReentrantLock(); + /** + * A {@link Condition} associated with {@link #mLock} for producer to unblock consumer. + */ + @NonNull + private final Condition mLockNotifier = mLock.newCondition(); + + @GuardedBy("mLock") + @NonNull + private final SparseArray<WriteTask> mPendingTasks = new SparseArray<>(); + + @GuardedBy("mLock") + private final IntArray mRemovedUsers = new IntArray(); + + @NonNull + private final Thread mWriterThread = new Thread("android.ime.as") { + + /** + * Waits until the next data has come then return the result after filtering out any + * already removed users. + * + * @return A list of {@link WriteTask} to be written into persistent storage + */ + @WorkerThread + private ArrayList<WriteTask> fetchNextTasks() { + final SparseArray<WriteTask> tasks; + final IntArray removedUsers; + mLock.lock(); + try { + while (true) { + if (mPendingTasks.size() != 0) { + tasks = mPendingTasks.clone(); + mPendingTasks.clear(); + if (mRemovedUsers.size() == 0) { + removedUsers = null; + } else { + removedUsers = mRemovedUsers.clone(); + } + break; + } + mLockNotifier.awaitUninterruptibly(); + } + } finally { + mLock.unlock(); + } + final int size = tasks.size(); + final ArrayList<WriteTask> result = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + final int userId = tasks.keyAt(i); + if (removedUsers != null && removedUsers.contains(userId)) { + continue; + } + result.add(tasks.valueAt(i)); + } + return result; + } + + @WorkerThread + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + while (true) { + final ArrayList<WriteTask> tasks = fetchNextTasks(); + tasks.forEach(task -> AdditionalSubtypeUtils.save( + task.subtypeMap, task.inputMethodMap, task.userId)); + } + } + }; + + /** + * Schedules a write operation + * + * @param userId the target user ID of this operation + * @param subtypeMap {@link AdditionalSubtypeMap} to be saved + * @param inputMethodMap {@link InputMethodMap} to be used to filter our {@code subtypeMap} + */ + @AnyThread + void scheduleWriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap, + @NonNull InputMethodMap inputMethodMap) { + final var task = new WriteTask(userId, subtypeMap, inputMethodMap); + mLock.lock(); + try { + if (mRemovedUsers.contains(userId)) { + return; + } + mPendingTasks.put(userId, task); + mLockNotifier.signalAll(); + } finally { + mLock.unlock(); + } + } + + /** + * Called back when a user is being created. + * + * @param userId The user ID to be created + */ + @AnyThread + void onUserCreated(@UserIdInt int userId) { + mLock.lock(); + try { + for (int i = mRemovedUsers.size() - 1; i >= 0; --i) { + if (mRemovedUsers.get(i) == userId) { + mRemovedUsers.remove(i); + } + } + } finally { + mLock.unlock(); + } + } + + /** + * Called back when a user is being removed. Any pending task will be effectively canceled + * if the user is removed before the task is fulfilled. + * + * @param userId The user ID to be removed + */ + @AnyThread + void onUserRemoved(@UserIdInt int userId) { + mLock.lock(); + try { + mRemovedUsers.add(userId); + mPendingTasks.remove(userId); + } finally { + mLock.unlock(); + } + } + + void startThread() { + mWriterThread.start(); + } + } + + private static final SingleThreadedBackgroundWriter sWriter = + new SingleThreadedBackgroundWriter(); + /** * Not intended to be instantiated. */ @@ -64,9 +218,11 @@ final class AdditionalSubtypeMapRepository { return; } sPerUserMap.put(userId, map); - // TODO: Offload this to a background thread. - // TODO: Skip if the previous data is exactly the same as new one. - AdditionalSubtypeUtils.save(map, inputMethodMap, userId); + sWriter.scheduleWriteTask(userId, map, inputMethodMap); + } + + static void startWriterThread() { + sWriter.startThread(); } static void initialize(@NonNull Handler handler, @NonNull Context context) { @@ -78,6 +234,7 @@ final class AdditionalSubtypeMapRepository { @Override public void onUserCreated(UserInfo user, @Nullable Object token) { final int userId = user.id; + sWriter.onUserCreated(userId); handler.post(() -> { synchronized (ImfLock.class) { if (!sPerUserMap.contains(userId)) { @@ -99,6 +256,7 @@ final class AdditionalSubtypeMapRepository { @Override public void onUserRemoved(UserInfo user) { final int userId = user.id; + sWriter.onUserRemoved(userId); handler.post(() -> { synchronized (ImfLock.class) { sPerUserMap.remove(userId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 5843d72f346a..7513c40a1f90 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1547,6 +1547,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), newSettings.getEnabledInputMethodList()); + + final var unused = SystemServerInitThreadPool.submit( + AdditionalSubtypeMapRepository::startWriterThread, + "Start AdditionalSubtypeMapRepository's writer thread"); } } } diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java index 363684f618cc..09605fefe80e 100644 --- a/services/core/java/com/android/server/media/MediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java @@ -59,7 +59,7 @@ abstract class MediaRoute2Provider { public abstract void requestCreateSession( long requestId, String packageName, - String routeId, + String routeOriginalId, @Nullable Bundle sessionHints, @RoutingSessionInfo.TransferReason int transferReason, @NonNull UserHandle transferInitiatorUserHandle, @@ -77,13 +77,15 @@ abstract class MediaRoute2Provider { long requestId, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName, - String sessionId, - String routeId, + String sessionOriginalId, + String routeOriginalId, @RoutingSessionInfo.TransferReason int transferReason); - public abstract void setRouteVolume(long requestId, String routeId, int volume); - public abstract void setSessionVolume(long requestId, String sessionId, int volume); - public abstract void prepareReleaseSession(@NonNull String sessionId); + public abstract void setRouteVolume(long requestId, String routeOriginalId, int volume); + + public abstract void setSessionVolume(long requestId, String sessionOriginalId, int volume); + + public abstract void prepareReleaseSession(@NonNull String sessionUniqueId); @NonNull public String getUniqueId() { @@ -197,8 +199,8 @@ abstract class MediaRoute2Provider { */ public final long mRequestId; - /** The {@link MediaRoute2Info#getId() id} of the target route. */ - @NonNull public final String mTargetRouteId; + /** The {@link MediaRoute2Info#getOriginalId()} original id} of the target route. */ + @NonNull public final String mTargetOriginalRouteId; @RoutingSessionInfo.TransferReason public final int mTransferReason; @@ -209,23 +211,23 @@ abstract class MediaRoute2Provider { SessionCreationOrTransferRequest( long requestId, - @NonNull String routeId, + @NonNull String targetOriginalRouteId, @RoutingSessionInfo.TransferReason int transferReason, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName) { mRequestId = requestId; - mTargetRouteId = routeId; + mTargetOriginalRouteId = targetOriginalRouteId; mTransferReason = transferReason; mTransferInitiatorUserHandle = transferInitiatorUserHandle; mTransferInitiatorPackageName = transferInitiatorPackageName; } public boolean isTargetRoute(@Nullable MediaRoute2Info route2Info) { - return route2Info != null && mTargetRouteId.equals(route2Info.getId()); + return route2Info != null && mTargetOriginalRouteId.equals(route2Info.getOriginalId()); } - public boolean isTargetRouteIdInList(@NonNull List<String> routesList) { - return routesList.stream().anyMatch(mTargetRouteId::equals); + public boolean isTargetRouteIdInList(@NonNull List<String> routeOriginalIdList) { + return routeOriginalIdList.stream().anyMatch(mTargetOriginalRouteId::equals); } } } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index 386657e99e36..71cbcb91100f 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -103,13 +103,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider public void requestCreateSession( long requestId, String packageName, - String routeId, + String routeOriginalId, Bundle sessionHints, @RoutingSessionInfo.TransferReason int transferReason, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName) { if (mConnectionReady) { - mActiveConnection.requestCreateSession(requestId, packageName, routeId, sessionHints); + mActiveConnection.requestCreateSession( + requestId, packageName, routeOriginalId, sessionHints); updateBinding(); } } @@ -153,35 +154,35 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider long requestId, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName, - String sessionId, - String routeId, + String sessionOriginalId, + String routeOriginalId, @RoutingSessionInfo.TransferReason int transferReason) { if (mConnectionReady) { - mActiveConnection.transferToRoute(requestId, sessionId, routeId); + mActiveConnection.transferToRoute(requestId, sessionOriginalId, routeOriginalId); } } @Override - public void setRouteVolume(long requestId, String routeId, int volume) { + public void setRouteVolume(long requestId, String routeOriginalId, int volume) { if (mConnectionReady) { - mActiveConnection.setRouteVolume(requestId, routeId, volume); + mActiveConnection.setRouteVolume(requestId, routeOriginalId, volume); updateBinding(); } } @Override - public void setSessionVolume(long requestId, String sessionId, int volume) { + public void setSessionVolume(long requestId, String sessionOriginalId, int volume) { if (mConnectionReady) { - mActiveConnection.setSessionVolume(requestId, sessionId, volume); + mActiveConnection.setSessionVolume(requestId, sessionOriginalId, volume); updateBinding(); } } @Override - public void prepareReleaseSession(@NonNull String sessionId) { + public void prepareReleaseSession(@NonNull String sessionUniqueId) { synchronized (mLock) { for (RoutingSessionInfo session : mSessionInfos) { - if (TextUtils.equals(session.getId(), sessionId)) { + if (TextUtils.equals(session.getId(), sessionUniqueId)) { mSessionInfos.remove(session); mReleasingSessions.add(session); break; diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 76930a003e46..6b409ee6f482 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -158,20 +158,20 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { public void requestCreateSession( long requestId, String packageName, - String routeId, + String routeOriginalId, Bundle sessionHints, @RoutingSessionInfo.TransferReason int transferReason, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName) { // Assume a router without MODIFY_AUDIO_ROUTING permission can't request with // a route ID different from the default route ID. The service should've filtered. - if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { + if (TextUtils.equals(routeOriginalId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo); return; } if (!Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { - if (TextUtils.equals(routeId, mSelectedRouteId)) { + if (TextUtils.equals(routeOriginalId, mSelectedRouteId)) { RoutingSessionInfo currentSessionInfo; synchronized (mLock) { currentSessionInfo = mSessionInfos.get(0); @@ -192,7 +192,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { mPendingSessionCreationOrTransferRequest = new SessionCreationOrTransferRequest( requestId, - routeId, + routeOriginalId, RoutingSessionInfo.TRANSFER_REASON_FALLBACK, transferInitiatorUserHandle, transferInitiatorPackageName); @@ -204,7 +204,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { transferInitiatorUserHandle, transferInitiatorPackageName, SYSTEM_SESSION_ID, - routeId, + routeOriginalId, transferReason); } @@ -234,15 +234,15 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { long requestId, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName, - String sessionId, - String routeId, + String sessionOriginalId, + String routeOriginalId, @RoutingSessionInfo.TransferReason int transferReason) { String selectedDeviceRouteId = mDeviceRouteController.getSelectedRoute().getId(); - if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { + if (TextUtils.equals(routeOriginalId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { // Transfer to the default route (which is the selected route). We replace the id to // be the selected route id so that the transfer reason gets updated. - routeId = selectedDeviceRouteId; + routeOriginalId = selectedDeviceRouteId; } else { Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT); return; @@ -254,18 +254,18 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { mPendingTransferRequest = new SessionCreationOrTransferRequest( requestId, - routeId, + routeOriginalId, transferReason, transferInitiatorUserHandle, transferInitiatorPackageName); } } - String finalRouteId = routeId; // Make a final copy to use it in the lambda. + String finalRouteId = routeOriginalId; // Make a final copy to use it in the lambda. boolean isAvailableDeviceRoute = mDeviceRouteController.getAvailableRoutes().stream() .anyMatch(it -> it.getId().equals(finalRouteId)); - boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRouteId); + boolean isSelectedDeviceRoute = TextUtils.equals(routeOriginalId, selectedDeviceRouteId); if (isSelectedDeviceRoute || isAvailableDeviceRoute) { // The requested route is managed by the device route controller. Note that the selected @@ -273,12 +273,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { // of the routing session). If the selected device route is transferred to, we need to // make the bluetooth routes inactive so that the device route becomes the selected // route of the routing session. - mDeviceRouteController.transferTo(routeId); + mDeviceRouteController.transferTo(routeOriginalId); mBluetoothRouteController.transferTo(null); } else { // The requested route is managed by the bluetooth route controller. mDeviceRouteController.transferTo(null); - mBluetoothRouteController.transferTo(routeId); + mBluetoothRouteController.transferTo(routeOriginalId); } if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses() @@ -288,20 +288,20 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } @Override - public void setRouteVolume(long requestId, String routeId, int volume) { - if (!TextUtils.equals(routeId, mSelectedRouteId)) { + public void setRouteVolume(long requestId, String routeOriginalId, int volume) { + if (!TextUtils.equals(routeOriginalId, mSelectedRouteId)) { return; } mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); } @Override - public void setSessionVolume(long requestId, String sessionId, int volume) { + public void setSessionVolume(long requestId, String sessionOriginalId, int volume) { // Do nothing since we don't support grouping volume yet. } @Override - public void prepareReleaseSession(String sessionId) { + public void prepareReleaseSession(String sessionUniqueId) { // Do nothing since the system session persists. } @@ -503,12 +503,13 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } long pendingRequestId = mPendingSessionCreationOrTransferRequest.mRequestId; - if (mPendingSessionCreationOrTransferRequest.mTargetRouteId.equals(mSelectedRouteId)) { + if (mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId.equals( + mSelectedRouteId)) { if (DEBUG) { Slog.w( TAG, "Session creation success to route " - + mPendingSessionCreationOrTransferRequest.mTargetRouteId); + + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId); } mPendingSessionCreationOrTransferRequest = null; mCallback.onSessionCreated(this, pendingRequestId, newSessionInfo); @@ -520,7 +521,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { Slog.w( TAG, "Session creation failed to route " - + mPendingSessionCreationOrTransferRequest.mTargetRouteId); + + mPendingSessionCreationOrTransferRequest + .mTargetOriginalRouteId); } mPendingSessionCreationOrTransferRequest = null; mCallback.onRequestFailed( @@ -529,7 +531,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { Slog.w( TAG, "Session creation waiting state to route " - + mPendingSessionCreationOrTransferRequest.mTargetRouteId); + + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId); } } } @@ -541,7 +543,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { // See b/307723189 for context for (MediaRoute2Info btRoute : mBluetoothRouteController.getAllBluetoothRoutes()) { if (TextUtils.equals( - btRoute.getId(), mPendingSessionCreationOrTransferRequest.mTargetRouteId)) { + btRoute.getId(), + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId)) { return true; } } diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index dec97fb588a5..0d1095f5656d 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -704,7 +704,8 @@ public class PackageArchiver { return false; } - if (isAppOptedOutOfArchiving(packageName, ps.getAppId())) { + if (isAppOptedOutOfArchiving(packageName, + UserHandle.getUid(userId, ps.getAppId()))) { return false; } diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index e00e81371853..f4b61e7b2d4a 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -72,7 +72,6 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; -import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.Xml; @@ -88,6 +87,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.server.SystemService; import com.android.server.servicewatcher.CurrentUserServiceSupplier; import com.android.server.servicewatcher.ServiceWatcher; +import com.android.server.utils.Slogf; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -98,7 +98,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.List; - +import java.util.Objects; /** * Manages trust agents and trust listeners. @@ -362,6 +362,13 @@ public class TrustManagerService extends SystemService { } private void scheduleTrustTimeout(boolean override, boolean isTrustableTimeout) { + if (DEBUG) { + Slogf.d( + TAG, + "scheduleTrustTimeout(override=%s, isTrustable=%s)", + override, + isTrustableTimeout); + } int shouldOverride = override ? 1 : 0; int trustableTimeout = isTrustableTimeout ? 1 : 0; mHandler.obtainMessage(MSG_SCHEDULE_TRUST_TIMEOUT, shouldOverride, @@ -370,6 +377,13 @@ public class TrustManagerService extends SystemService { private void handleScheduleTrustTimeout(boolean shouldOverride, TimeoutType timeoutType) { int userId = mCurrentUser; + if (DEBUG) { + Slogf.d( + TAG, + "handleScheduleTrustTimeout(shouldOverride=%s, timeoutType=%s)", + shouldOverride, + timeoutType); + } if (timeoutType == TimeoutType.TRUSTABLE) { // don't override the hard timeout unless biometric or knowledge factor authentication // occurs which isn't where this is called from. Override the idle timeout what the @@ -383,6 +397,7 @@ public class TrustManagerService extends SystemService { /* Override both the idle and hard trustable timeouts */ private void refreshTrustableTimers(int userId) { + if (DEBUG) Slogf.d(TAG, "refreshTrustableTimers(userId=%s)", userId); handleScheduleTrustableTimeouts(userId, true /* overrideIdleTimeout */, true /* overrideHardTimeout */); } @@ -405,13 +420,20 @@ public class TrustManagerService extends SystemService { } private void handleScheduleTrustedTimeout(int userId, boolean shouldOverride) { + if (DEBUG) { + Slogf.d( + TAG, + "handleScheduleTrustedTimeout(userId=%s, shouldOverride=%s)", + userId, + shouldOverride); + } long when = SystemClock.elapsedRealtime() + TRUST_TIMEOUT_IN_MILLIS; TrustedTimeoutAlarmListener alarm = mTrustTimeoutAlarmListenerForUser.get(userId); // Cancel existing trust timeouts for this user if needed. if (alarm != null) { if (!shouldOverride && alarm.isQueued()) { - if (DEBUG) Slog.d(TAG, "Found existing trust timeout alarm. Skipping."); + if (DEBUG) Slogf.d(TAG, "Found existing trust timeout alarm. Skipping."); return; } mAlarmManager.cancel(alarm); @@ -420,7 +442,9 @@ public class TrustManagerService extends SystemService { mTrustTimeoutAlarmListenerForUser.put(userId, alarm); } - if (DEBUG) Slog.d(TAG, "\tSetting up trust timeout alarm"); + if (DEBUG) { + Slogf.d(TAG, "\tSetting up trust timeout alarm triggering at elapsedRealTime=%s", when); + } alarm.setQueued(true /* isQueued */); mAlarmManager.setExact( AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm, @@ -434,6 +458,13 @@ public class TrustManagerService extends SystemService { } private void setUpIdleTimeout(int userId, boolean overrideIdleTimeout) { + if (DEBUG) { + Slogf.d( + TAG, + "setUpIdleTimeout(userId=%s, overrideIdleTimeout=%s)", + userId, + overrideIdleTimeout); + } long when = SystemClock.elapsedRealtime() + TRUSTABLE_IDLE_TIMEOUT_IN_MILLIS; TrustableTimeoutAlarmListener alarm = mIdleTrustableTimeoutAlarmListenerForUser.get(userId); mContext.enforceCallingOrSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM, null); @@ -441,7 +472,7 @@ public class TrustManagerService extends SystemService { // Cancel existing trustable timeouts for this user if needed. if (alarm != null) { if (!overrideIdleTimeout && alarm.isQueued()) { - if (DEBUG) Slog.d(TAG, "Found existing trustable timeout alarm. Skipping."); + if (DEBUG) Slogf.d(TAG, "Found existing trustable timeout alarm. Skipping."); return; } mAlarmManager.cancel(alarm); @@ -450,7 +481,12 @@ public class TrustManagerService extends SystemService { mIdleTrustableTimeoutAlarmListenerForUser.put(userId, alarm); } - if (DEBUG) Slog.d(TAG, "\tSetting up trustable idle timeout alarm"); + if (DEBUG) { + Slogf.d( + TAG, + "\tSetting up trustable idle timeout alarm triggering at elapsedRealTime=%s", + when); + } alarm.setQueued(true /* isQueued */); mAlarmManager.setExact( AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm, @@ -458,6 +494,13 @@ public class TrustManagerService extends SystemService { } private void setUpHardTimeout(int userId, boolean overrideHardTimeout) { + if (DEBUG) { + Slogf.i( + TAG, + "setUpHardTimeout(userId=%s, overrideHardTimeout=%s)", + userId, + overrideHardTimeout); + } mContext.enforceCallingOrSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM, null); TrustableTimeoutAlarmListener alarm = mTrustableTimeoutAlarmListenerForUser.get(userId); @@ -472,7 +515,13 @@ public class TrustManagerService extends SystemService { } else if (overrideHardTimeout) { mAlarmManager.cancel(alarm); } - if (DEBUG) Slog.d(TAG, "\tSetting up trustable hard timeout alarm"); + if (DEBUG) { + Slogf.d( + TAG, + "\tSetting up trustable hard timeout alarm triggering at " + + "elapsedRealTime=%s", + when); + } alarm.setQueued(true /* isQueued */); mAlarmManager.setExact( AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm, @@ -503,6 +552,12 @@ public class TrustManagerService extends SystemService { public int hashCode() { return component.hashCode() * 31 + userId; } + + @Override + public String toString() { + return String.format( + "AgentInfo{label=%s, component=%s, userId=%s}", label, component, userId); + } } private void updateTrustAll() { @@ -532,6 +587,15 @@ public class TrustManagerService extends SystemService { int flags, boolean isFromUnlock, @Nullable AndroidFuture<GrantTrustResult> resultCallback) { + if (DEBUG) { + Slogf.d( + TAG, + "updateTrust(userId=%s, flags=%s, isFromUnlock=%s, resultCallbackPresent=%s)", + userId, + flags, + isFromUnlock, + Objects.isNull(resultCallback)); + } boolean managed = aggregateIsTrustManaged(userId); dispatchOnTrustManagedChanged(managed, userId); if (mStrongAuthTracker.isTrustAllowedForUser(userId) @@ -559,27 +623,50 @@ public class TrustManagerService extends SystemService { (flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0); boolean canMoveToTrusted = alreadyUnlocked || isFromUnlock || renewingTrust || isAutomotive(); - boolean upgradingTrustForCurrentUser = (userId == mCurrentUser); + boolean updatingTrustForCurrentUser = (userId == mCurrentUser); + + if (DEBUG) { + Slogf.d( + TAG, + "updateTrust: alreadyUnlocked=%s, wasTrusted=%s, wasTrustable=%s, " + + "renewingTrust=%s, canMoveToTrusted=%s, " + + "updatingTrustForCurrentUser=%s", + alreadyUnlocked, + wasTrusted, + wasTrustable, + renewingTrust, + canMoveToTrusted, + updatingTrustForCurrentUser); + } if (trustedByAtLeastOneAgent && wasTrusted) { // no change return; - } else if (trustedByAtLeastOneAgent && canMoveToTrusted - && upgradingTrustForCurrentUser) { + } else if (trustedByAtLeastOneAgent + && canMoveToTrusted + && updatingTrustForCurrentUser) { pendingTrustState = TrustState.TRUSTED; - } else if (trustableByAtLeastOneAgent && (wasTrusted || wasTrustable) - && upgradingTrustForCurrentUser) { + } else if (trustableByAtLeastOneAgent + && (wasTrusted || wasTrustable) + && updatingTrustForCurrentUser) { pendingTrustState = TrustState.TRUSTABLE; } else { pendingTrustState = TrustState.UNTRUSTED; } + if (DEBUG) Slogf.d(TAG, "updateTrust: pendingTrustState=%s", pendingTrustState); mUserTrustState.put(userId, pendingTrustState); } - if (DEBUG) Slog.d(TAG, "pendingTrustState: " + pendingTrustState); boolean isNowTrusted = pendingTrustState == TrustState.TRUSTED; boolean newlyUnlocked = !alreadyUnlocked && isNowTrusted; + if (DEBUG) { + Slogf.d( + TAG, + "updateTrust: isNowTrusted=%s, newlyUnlocked=%s", + isNowTrusted, + newlyUnlocked); + } maybeActiveUnlockRunningChanged(userId); dispatchOnTrustChanged( isNowTrusted, newlyUnlocked, userId, flags, getTrustGrantedMessages(userId)); @@ -598,13 +685,13 @@ public class TrustManagerService extends SystemService { boolean shouldSendCallback = newlyUnlocked; if (shouldSendCallback) { if (resultCallback != null) { - if (DEBUG) Slog.d(TAG, "calling back with UNLOCKED_BY_GRANT"); + if (DEBUG) Slogf.d(TAG, "calling back with UNLOCKED_BY_GRANT"); resultCallback.complete(new GrantTrustResult(STATUS_UNLOCKED_BY_GRANT)); } } if ((wasTrusted || wasTrustable) && pendingTrustState == TrustState.UNTRUSTED) { - if (DEBUG) Slog.d(TAG, "Trust was revoked, destroy trustable alarms"); + if (DEBUG) Slogf.d(TAG, "Trust was revoked, destroy trustable alarms"); cancelBothTrustableAlarms(userId); } } @@ -650,7 +737,7 @@ public class TrustManagerService extends SystemService { try { WindowManagerGlobal.getWindowManagerService().lockNow(null); } catch (RemoteException e) { - Slog.e(TAG, "Error locking screen when called from trust agent"); + Slogf.e(TAG, "Error locking screen when called from trust agent"); } } @@ -659,8 +746,9 @@ public class TrustManagerService extends SystemService { } void refreshAgentList(int userIdOrAll) { - if (DEBUG) Slog.d(TAG, "refreshAgentList(" + userIdOrAll + ")"); + if (DEBUG) Slogf.d(TAG, "refreshAgentList(userIdOrAll=%s)", userIdOrAll); if (!mTrustAgentsCanRun) { + if (DEBUG) Slogf.d(TAG, "Did not refresh agent list because agents cannot run."); return; } if (userIdOrAll != UserHandle.USER_ALL && userIdOrAll < UserHandle.USER_SYSTEM) { @@ -686,18 +774,30 @@ public class TrustManagerService extends SystemService { if (userInfo == null || userInfo.partial || !userInfo.isEnabled() || userInfo.guestToRemove) continue; if (!userInfo.supportsSwitchToByUser()) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": switchToByUser=false"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: switchToByUser=false", + userInfo.id); + } continue; } if (!mActivityManager.isUserRunning(userInfo.id)) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": user not started"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: user not started", + userInfo.id); + } continue; } if (!lockPatternUtils.isSecure(userInfo.id)) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": no secure credential"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: no secure credential", + userInfo.id); + } continue; } @@ -708,8 +808,12 @@ public class TrustManagerService extends SystemService { List<ComponentName> enabledAgents = lockPatternUtils.getEnabledTrustAgents(userInfo.id); if (enabledAgents.isEmpty()) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": no agents enabled by user"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: no agents enabled by user", + userInfo.id); + } continue; } List<ResolveInfo> resolveInfos = resolveAllowedTrustAgents(pm, userInfo.id); @@ -717,9 +821,13 @@ public class TrustManagerService extends SystemService { ComponentName name = getComponentName(resolveInfo); if (!enabledAgents.contains(name)) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping " - + name.flattenToShortString() + " u"+ userInfo.id - + ": not enabled by user"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping %s u%s: not enabled by user", + name.flattenToShortString(), + userInfo.id); + } continue; } if (disableTrustAgents) { @@ -727,9 +835,13 @@ public class TrustManagerService extends SystemService { dpm.getTrustAgentConfiguration(null /* admin */, name, userInfo.id); // Disable agent if no features are enabled. if (config == null || config.isEmpty()) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping " - + name.flattenToShortString() + " u"+ userInfo.id - + ": not allowed by DPM"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping %s u%s: not allowed by DPM", + name.flattenToShortString(), + userInfo.id); + } continue; } } @@ -752,15 +864,26 @@ public class TrustManagerService extends SystemService { } if (directUnlock) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: trustagent " + name - + "of user " + userInfo.id + "can unlock user profile."); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: trustagent %s of user %s can unlock user " + + "profile.", + name, + userInfo.id); + } } if (!mUserManager.isUserUnlockingOrUnlocked(userInfo.id) && !directUnlock) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + "'s trust agent " + name + ": FBE still locked and " - + " the agent cannot unlock user profile."); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s's trust agent %s: FBE still " + + "locked and the agent cannot unlock user profile.", + userInfo.id, + name); + } continue; } @@ -769,11 +892,16 @@ public class TrustManagerService extends SystemService { if (flag != StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) { if (flag != StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT || !directUnlock) { - if (DEBUG) - Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": prevented by StrongAuthTracker = 0x" - + Integer.toHexString(mStrongAuthTracker.getStrongAuthForUser( - userInfo.id))); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: prevented by " + + "StrongAuthTracker = 0x%s", + userInfo.id, + Integer.toHexString( + mStrongAuthTracker.getStrongAuthForUser( + userInfo.id))); + } continue; } } @@ -804,6 +932,15 @@ public class TrustManagerService extends SystemService { } } + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: userInfos=%s, obsoleteAgents=%s, trustMayHaveChanged=%s", + userInfos, + obsoleteAgents, + trustMayHaveChanged); + } + if (trustMayHaveChanged) { if (userIdOrAll == UserHandle.USER_ALL) { updateTrustAll(); @@ -1044,7 +1181,7 @@ public class TrustManagerService extends SystemService { parser = resolveInfo.serviceInfo.loadXmlMetaData(pm, TrustAgentService.TRUST_AGENT_META_DATA); if (parser == null) { - Slog.w(TAG, "Can't find " + TrustAgentService.TRUST_AGENT_META_DATA + " meta-data"); + Slogf.w(TAG, "Can't find %s meta-data", TrustAgentService.TRUST_AGENT_META_DATA); return null; } Resources res = pm.getResourcesForApplication(resolveInfo.serviceInfo.applicationInfo); @@ -1056,7 +1193,7 @@ public class TrustManagerService extends SystemService { } String nodeName = parser.getName(); if (!"trust-agent".equals(nodeName)) { - Slog.w(TAG, "Meta-data does not start with trust-agent tag"); + Slogf.w(TAG, "Meta-data does not start with trust-agent tag"); return null; } TypedArray sa = res @@ -1075,7 +1212,11 @@ public class TrustManagerService extends SystemService { if (parser != null) parser.close(); } if (caughtException != null) { - Slog.w(TAG, "Error parsing : " + resolveInfo.serviceInfo.packageName, caughtException); + Slogf.w( + TAG, + caughtException, + "Error parsing : %s", + resolveInfo.serviceInfo.packageName); return null; } if (cn == null) { @@ -1242,13 +1383,18 @@ public class TrustManagerService extends SystemService { // Agent dispatch and aggregation private boolean aggregateIsTrusted(int userId) { + if (DEBUG) Slogf.d(TAG, "aggregateIsTrusted(userId=%s)", userId); if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) { + if (DEBUG) { + Slogf.d(TAG, "not trusted because trust not allowed for userId=%s", userId); + } return false; } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); if (info.userId == userId) { if (info.agent.isTrusted()) { + if (DEBUG) Slogf.d(TAG, "trusted by %s", info); return true; } } @@ -1257,13 +1403,18 @@ public class TrustManagerService extends SystemService { } private boolean aggregateIsTrustable(int userId) { + if (DEBUG) Slogf.d(TAG, "aggregateIsTrustable(userId=%s)", userId); if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) { + if (DEBUG) { + Slogf.d(TAG, "not trustable because trust not allowed for userId=%s", userId); + } return false; } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); if (info.userId == userId) { if (info.agent.isTrustable()) { + if (DEBUG) Slogf.d(TAG, "trustable by %s", info); return true; } } @@ -1328,20 +1479,31 @@ public class TrustManagerService extends SystemService { private boolean aggregateIsTrustManaged(int userId) { if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) { + if (DEBUG) { + Slogf.d( + TAG, + "trust not managed due to trust not being allowed for userId=%s", + userId); + } return false; } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); if (info.userId == userId) { if (info.agent.isManagingTrust()) { + if (DEBUG) Slogf.d(TAG, "trust managed for userId=%s", userId); return true; } } } + if (DEBUG) Slogf.d(TAG, "trust not managed for userId=%s", userId); return false; } private void dispatchUnlockAttempt(boolean successful, int userId) { + if (DEBUG) { + Slogf.d(TAG, "dispatchUnlockAttempt(successful=%s, userId=%s)", successful, userId); + } if (successful) { mStrongAuthTracker.allowTrustFromUnlock(userId); // Allow the presence of trust on a successful unlock attempt to extend unlock @@ -1359,8 +1521,11 @@ public class TrustManagerService extends SystemService { private void dispatchUserRequestedUnlock(int userId, boolean dismissKeyguard) { if (DEBUG) { - Slog.d(TAG, "dispatchUserRequestedUnlock(user=" + userId + ", dismissKeyguard=" - + dismissKeyguard + ")"); + Slogf.d( + TAG, + "dispatchUserRequestedUnlock(user=%s, dismissKeyguard=%s)", + userId, + dismissKeyguard); } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); @@ -1372,7 +1537,7 @@ public class TrustManagerService extends SystemService { private void dispatchUserMayRequestUnlock(int userId) { if (DEBUG) { - Slog.d(TAG, "dispatchUserMayRequestUnlock(user=" + userId + ")"); + Slogf.d(TAG, "dispatchUserMayRequestUnlock(user=%s)", userId); } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); @@ -1405,9 +1570,9 @@ public class TrustManagerService extends SystemService { try { listener.onIsActiveUnlockRunningChanged(isRunning, userId); } catch (DeadObjectException e) { - Slog.d(TAG, "TrustListener dead while trying to notify Active Unlock running state"); + Slogf.d(TAG, "TrustListener dead while trying to notify Active Unlock running state"); } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } @@ -1445,11 +1610,11 @@ public class TrustManagerService extends SystemService { mTrustListeners.get(i).onTrustChanged( enabled, newlyUnlocked, userId, flags, trustGrantedMessages); } catch (DeadObjectException e) { - Slog.d(TAG, "Removing dead TrustListener."); + Slogf.d(TAG, "Removing dead TrustListener."); mTrustListeners.remove(i); i--; } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } } @@ -1462,11 +1627,11 @@ public class TrustManagerService extends SystemService { try { mTrustListeners.get(i).onEnabledTrustAgentsChanged(userId); } catch (DeadObjectException e) { - Slog.d(TAG, "Removing dead TrustListener."); + Slogf.d(TAG, "Removing dead TrustListener."); mTrustListeners.remove(i); i--; } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } } @@ -1479,11 +1644,11 @@ public class TrustManagerService extends SystemService { try { mTrustListeners.get(i).onTrustManagedChanged(managed, userId); } catch (DeadObjectException e) { - Slog.d(TAG, "Removing dead TrustListener."); + Slogf.d(TAG, "Removing dead TrustListener."); mTrustListeners.remove(i); i--; } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } } @@ -1496,11 +1661,11 @@ public class TrustManagerService extends SystemService { try { mTrustListeners.get(i).onTrustError(message); } catch (DeadObjectException e) { - Slog.d(TAG, "Removing dead TrustListener."); + Slogf.d(TAG, "Removing dead TrustListener."); mTrustListeners.remove(i); i--; } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } } @@ -1535,7 +1700,7 @@ public class TrustManagerService extends SystemService { && mFingerprintManager.hasEnrolledTemplates(userId) && isWeakOrConvenienceSensor( mFingerprintManager.getSensorProperties().get(0))) { - Slog.i(TAG, "User is unlockable by non-strong fingerprint auth"); + Slogf.i(TAG, "User is unlockable by non-strong fingerprint auth"); return true; } @@ -1543,7 +1708,7 @@ public class TrustManagerService extends SystemService { && (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_FACE) == 0 && mFaceManager.hasEnrolledTemplates(userId) && isWeakOrConvenienceSensor(mFaceManager.getSensorProperties().get(0))) { - Slog.i(TAG, "User is unlockable by non-strong face auth"); + Slogf.i(TAG, "User is unlockable by non-strong face auth"); return true; } } @@ -1551,7 +1716,7 @@ public class TrustManagerService extends SystemService { // Check whether it's possible for the device to be actively unlocked by a trust agent. if (getUserTrustStateInner(userId) == TrustState.TRUSTABLE || (isAutomotive() && isTrustUsuallyManagedInternal(userId))) { - Slog.i(TAG, "User is unlockable by trust agent"); + Slogf.i(TAG, "User is unlockable by trust agent"); return true; } @@ -1595,6 +1760,13 @@ public class TrustManagerService extends SystemService { private final IBinder mService = new ITrustManager.Stub() { @Override public void reportUnlockAttempt(boolean authenticated, int userId) throws RemoteException { + if (DEBUG) { + Slogf.d( + TAG, + "reportUnlockAttempt(authenticated=%s, userId=%s)", + authenticated, + userId); + } enforceReportPermission(); mHandler.obtainMessage(MSG_DISPATCH_UNLOCK_ATTEMPT, authenticated ? 1 : 0, userId) .sendToTarget(); @@ -1611,7 +1783,8 @@ public class TrustManagerService extends SystemService { @Override public void reportUserMayRequestUnlock(int userId) throws RemoteException { enforceReportPermission(); - mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /*arg2=*/ 0).sendToTarget(); + mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /* arg2= */ 0) + .sendToTarget(); } @Override @@ -1932,6 +2105,7 @@ public class TrustManagerService extends SystemService { return new Handler(looper) { @Override public void handleMessage(Message msg) { + if (DEBUG) Slogf.d(TAG, "handler: %s", msg.what); switch (msg.what) { case MSG_REGISTER_LISTENER: addListener((ITrustListener) msg.obj); @@ -2002,8 +2176,24 @@ public class TrustManagerService extends SystemService { handleScheduleTrustTimeout(shouldOverride, timeoutType); break; case MSG_REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH: + if (DEBUG) { + Slogf.d(TAG, "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH userId=%s", msg.arg1); + } TrustableTimeoutAlarmListener trustableAlarm = mTrustableTimeoutAlarmListenerForUser.get(msg.arg1); + if (DEBUG) { + if (trustableAlarm != null) { + Slogf.d( + TAG, + "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH trustable alarm " + + "isQueued=%s", + trustableAlarm.mIsQueued); + } else { + Slogf.d( + TAG, + "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH no trustable alarm"); + } + } if (trustableAlarm != null && trustableAlarm.isQueued()) { refreshTrustableTimers(msg.arg1); } @@ -2194,7 +2384,7 @@ public class TrustManagerService extends SystemService { handleAlarm(); // Only fire if trust can unlock. if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) { - if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout"); + if (DEBUG) Slogf.d(TAG, "Revoking all trust because of trust timeout"); mLockPatternUtils.requireStrongAuth( mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 86115990f746..2d2a88a866ba 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3964,7 +3964,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (isCurrentVisible) { - if (isNextNotYetVisible || delayRemoval || isInTransition()) { + if (isNextNotYetVisible || delayRemoval || (next != null && isInTransition())) { // Add this activity to the list of stopping activities. It will be processed and // destroyed when the next activity reports idle. addToStopping(false /* scheduleIdle */, false /* idleDelayed */, @@ -7644,6 +7644,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // This could only happen when the window is removed from hierarchy. So do not keep its // reference anymore. mStartingWindow = null; + mStartingData = null; + mStartingSurface = null; } if (mChildren.size() == 0 && mVisibleSetFromTransferredStartingWindow) { // We set the visible state to true for the token from a transferred starting @@ -9259,6 +9261,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @NonNull CompatDisplayInsets compatDisplayInsets) { final Configuration resolvedConfig = getResolvedOverrideConfiguration(); final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); + final Insets insets; + if (mResolveConfigHint.mUseOverrideInsetsForConfig) { + // TODO(b/343197837): Add test to verify SCM behaviour with new bound configuration + // Insets are decoupled from configuration by default from V+, use legacy + // compatibility behaviour for apps targeting SDK earlier than 35 + // (see applySizeOverrideIfNeeded). + insets = Insets.of(mDisplayContent.getDisplayPolicy() + .getDecorInsetsInfo(mDisplayContent.mDisplayFrames.mRotation, + mDisplayContent.mDisplayFrames.mWidth, + mDisplayContent.mDisplayFrames.mHeight).mOverrideNonDecorInsets); + } else { + insets = Insets.NONE; + } // When an activity needs to be letterboxed because of fixed orientation, use fixed // orientation bounds (stored in resolved bounds) instead of parent bounds since the @@ -9269,9 +9284,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final Rect containerBounds = useResolvedBounds ? new Rect(resolvedBounds) : newParentConfiguration.windowConfiguration.getBounds(); + final Rect parentAppBounds = + newParentConfiguration.windowConfiguration.getAppBounds(); + parentAppBounds.inset(insets); final Rect containerAppBounds = useResolvedBounds ? new Rect(resolvedConfig.windowConfiguration.getAppBounds()) - : newParentConfiguration.windowConfiguration.getAppBounds(); + : parentAppBounds; final int requestedOrientation = getRequestedConfigurationOrientation(); final boolean orientationRequested = requestedOrientation != ORIENTATION_UNDEFINED; diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index ac2c886d1b66..207707efb51d 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -28,7 +28,6 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static android.os.Process.INVALID_PID; import static android.os.Process.INVALID_UID; -import static android.os.Process.ROOT_UID; import static android.os.Process.SYSTEM_UID; import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; @@ -386,10 +385,6 @@ public class BackgroundActivityStartController { return BackgroundStartPrivileges.NONE; case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: // no explicit choice by the app - let us decide what to do - if (callingUid == ROOT_UID || callingUid == SYSTEM_UID) { - // root and system must always opt in explicitly - return BackgroundStartPrivileges.NONE; - } if (callingPackage != null) { // determine based on the calling/creating package boolean changeEnabled = CompatChanges.isChangeEnabled( diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 94a22394cf41..a00b6fc47de7 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1927,7 +1927,7 @@ public class WindowManagerService extends IWindowManager.Stub displayContent.getInsetsStateController().updateAboveInsetsState( false /* notifyInsetsChanged */); - outInsetsState.set(win.getCompatInsetsState(), true /* copySources */); + win.fillInsetsState(outInsetsState, true /* copySources */); getInsetsSourceControls(win, outActiveControls); if (win.mLayoutAttached) { @@ -2680,7 +2680,7 @@ public class WindowManagerService extends IWindowManager.Stub } if (outInsetsState != null) { - outInsetsState.set(win.getCompatInsetsState(), true /* copySources */); + win.fillInsetsState(outInsetsState, true /* copySources */); } ProtoLog.v(WM_DEBUG_FOCUS, "Relayout of %s: focusMayChange=%b", @@ -2743,12 +2743,10 @@ public class WindowManagerService extends IWindowManager.Stub } private void getInsetsSourceControls(WindowState win, InsetsSourceControl.Array outArray) { - final InsetsSourceControl[] controls = - win.getDisplayContent().getInsetsStateController().getControlsForDispatch(win); // We will leave the critical section before returning the leash to the client, // so we need to copy the leash to prevent others release the one that we are // about to return. - outArray.set(controls, true /* copyControls */); + win.fillInsetsSourceControls(outArray, true /* copyControls */); // This source control is an extra copy if the client is not local. By setting // PARCELABLE_WRITE_RETURN_VALUE, the leash will be released at the end of // SurfaceControl.writeToParcel. diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index d7c49ac81a6c..d1efc27d9661 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -434,7 +434,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP /** @see #isLastConfigReportedToClient() */ private boolean mLastConfigReportedToClient; - // TODO(b/339380439): Ensure to use the same object for IWindowSession#relayout + private final ClientWindowFrames mLastReportedFrames = new ClientWindowFrames(); + + private final InsetsState mLastReportedInsetsState = new InsetsState(); + private final InsetsSourceControl.Array mLastReportedActiveControls = new InsetsSourceControl.Array(); @@ -495,8 +498,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP private final WindowFrames mWindowFrames = new WindowFrames(); - private final ClientWindowFrames mClientWindowFrames = new ClientWindowFrames(); - /** * List of rects where system gestures should be ignored. * @@ -3650,8 +3651,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP outFrames.attachedFrame.scale(mInvGlobalScale); } } - outFrames.compatScale = getCompatScaleForClient(); + if (mLastReportedFrames != outFrames) { + mLastReportedFrames.setTo(outFrames); + } // Note: in the cases where the window is tied to an activity, we should not send a // configuration update when the window has requested to be hidden. Doing so can lead to @@ -3678,6 +3681,25 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mLastConfigReportedToClient = true; } + void fillInsetsState(@NonNull InsetsState outInsetsState, boolean copySources) { + outInsetsState.set(getCompatInsetsState(), copySources); + if (outInsetsState != mLastReportedInsetsState) { + // No need to copy for the recorded. + mLastReportedInsetsState.set(outInsetsState, false /* copySources */); + } + } + + void fillInsetsSourceControls(@NonNull InsetsSourceControl.Array outArray, + boolean copyControls) { + final InsetsSourceControl[] controls = + getDisplayContent().getInsetsStateController().getControlsForDispatch(this); + outArray.set(controls, copyControls); + if (outArray != mLastReportedActiveControls) { + // No need to copy for the recorded. + mLastReportedActiveControls.setTo(outArray, false /* copyControls */); + } + } + void reportResized() { // If the activity is scheduled to relaunch, skip sending the resized to ViewRootImpl now // since it will be destroyed anyway. This also prevents the client from receiving @@ -3712,9 +3734,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int prevRotation = mLastReportedConfiguration .getMergedConfiguration().windowConfiguration.getRotation(); - fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration, + fillClientWindowFramesAndConfiguration(mLastReportedFrames, mLastReportedConfiguration, mLastReportedActivityWindowInfo, true /* useLatestConfig */, false /* relayoutVisible */); + fillInsetsState(mLastReportedInsetsState, false /* copySources */); final boolean syncRedraw = shouldSendRedrawForSync(); final boolean syncWithBuffers = syncRedraw && shouldSyncWithBuffers(); final boolean reportDraw = syncRedraw || drawPending; @@ -3734,8 +3757,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (Flags.bundleClientTransactionFlag()) { getProcess().scheduleClientTransactionItem( - WindowStateResizeItem.obtain(mClient, mClientWindowFrames, reportDraw, - mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, + WindowStateResizeItem.obtain(mClient, mLastReportedFrames, reportDraw, + mLastReportedConfiguration, mLastReportedInsetsState, forceRelayout, alwaysConsumeSystemBars, displayId, syncWithBuffers ? mSyncSeqId : -1, isDragResizing, mLastReportedActivityWindowInfo)); @@ -3743,8 +3766,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } else { // TODO(b/301870955): cleanup after launch try { - mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration, - getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, + mClient.resized(mLastReportedFrames, reportDraw, mLastReportedConfiguration, + mLastReportedInsetsState, forceRelayout, alwaysConsumeSystemBars, displayId, syncWithBuffers ? mSyncSeqId : -1, isDragResizing, mLastReportedActivityWindowInfo); onResizePostDispatched(drawPending, prevRotation, displayId); @@ -3817,17 +3840,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mRemoved) { return; } - final InsetsStateController stateController = - getDisplayContent().getInsetsStateController(); - final InsetsState insetsState = getCompatInsetsState(); - mLastReportedActiveControls.set(stateController.getControlsForDispatch(this), - false /* copyControls */); + fillInsetsState(mLastReportedInsetsState, false /* copySources */); + fillInsetsSourceControls(mLastReportedActiveControls, false /* copyControls */); if (Flags.insetsControlChangedItem()) { getProcess().scheduleClientTransactionItem(WindowStateInsetsControlChangeItem.obtain( - mClient, insetsState, mLastReportedActiveControls)); + mClient, mLastReportedInsetsState, mLastReportedActiveControls)); } else { try { - mClient.insetsControlChanged(insetsState, mLastReportedActiveControls); + mClient.insetsControlChanged(mLastReportedInsetsState, mLastReportedActiveControls); } catch (RemoteException e) { Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java index 52b33db556e6..d4f0d5aa7ef6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java @@ -593,6 +593,12 @@ public class ProxyManagerTest { } @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) + throws RemoteException { + + } + + @Override public void onMagnificationChanged(int displayId, Region region, MagnificationConfig config) throws RemoteException { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java index 87fe6cf8f283..0de5807067e6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java @@ -59,6 +59,7 @@ import android.view.accessibility.MagnificationAnimationCallback; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.FlakyTest; +import com.android.compatibility.common.util.TestUtils; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityTraceManager; @@ -76,6 +77,7 @@ import org.mockito.invocation.InvocationOnMock; */ public class MagnificationConnectionManagerTest { + private static final int WAIT_CONNECTION_TIMEOUT_SECOND = 1; private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM; private static final int SERVICE_ID = 1; @@ -115,17 +117,19 @@ public class MagnificationConnectionManagerTest { private void stubSetConnection(boolean needDelay) { doAnswer((InvocationOnMock invocation) -> { final boolean connect = (Boolean) invocation.getArguments()[0]; - // Simulates setConnection() called by another process. + // Use post to simulate setConnection() called by another process. + final Context context = ApplicationProvider.getApplicationContext(); if (needDelay) { - final Context context = ApplicationProvider.getApplicationContext(); context.getMainThreadHandler().postDelayed( () -> { mMagnificationConnectionManager.setConnection( connect ? mMockConnection.getConnection() : null); }, 10); } else { - mMagnificationConnectionManager.setConnection( - connect ? mMockConnection.getConnection() : null); + context.getMainThreadHandler().post(() -> { + mMagnificationConnectionManager.setConnection( + connect ? mMockConnection.getConnection() : null); + }); } return true; }).when(mMockStatusBarManagerInternal).requestMagnificationConnection(anyBoolean()); @@ -629,9 +633,10 @@ public class MagnificationConnectionManagerTest { } @Test - public void isConnected_requestConnection_expectedValue() throws RemoteException { + public void isConnected_requestConnection_expectedValue() throws Exception { mMagnificationConnectionManager.requestConnection(true); - assertTrue(mMagnificationConnectionManager.isConnected()); + TestUtils.waitUntil("connection is not ready", WAIT_CONNECTION_TIMEOUT_SECOND, + () -> mMagnificationConnectionManager.isConnected()); mMagnificationConnectionManager.requestConnection(false); assertFalse(mMagnificationConnectionManager.isConnected()); diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java index e756082bc912..758c84a26dcd 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.AppOpsManager; import android.content.Context; import android.content.res.Resources; import android.media.AudioDeviceAttributes; @@ -37,6 +38,7 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.IAudioDeviceVolumeDispatcher; import android.media.VolumeInfo; +import android.os.PermissionEnforcer; import android.os.RemoteException; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -98,7 +100,8 @@ public class AbsoluteVolumeBehaviorTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, - mTestLooper.getLooper()) { + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java index 3623012b348f..2cb02bdd2806 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java @@ -23,12 +23,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.app.AppOpsManager; import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.AudioSystem; import android.media.VolumeInfo; +import android.os.PermissionEnforcer; import android.os.test.TestLooper; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; @@ -75,7 +77,8 @@ public class AudioDeviceVolumeManagerTest { mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase(); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, - mTestLooper.getLooper()) { + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java new file mode 100644 index 000000000000..8d772ad5c124 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java @@ -0,0 +1,308 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.audio; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.any; +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.when; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.media.permission.INativePermissionController; +import com.android.media.permission.UidPackageState; +import com.android.server.pm.pkg.PackageState; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@RunWith(AndroidJUnit4.class) +@Presubmit +public final class AudioServerPermissionProviderTest { + + // Class under test + private AudioServerPermissionProvider mPermissionProvider; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock public INativePermissionController mMockPc; + + @Mock public PackageState mMockPackageStateOne_10000_one; + @Mock public PackageState mMockPackageStateTwo_10001_two; + @Mock public PackageState mMockPackageStateThree_10000_one; + @Mock public PackageState mMockPackageStateFour_10000_three; + @Mock public PackageState mMockPackageStateFive_10001_four; + @Mock public PackageState mMockPackageStateSix_10000_two; + + public List<UidPackageState> mInitPackageListExpected; + + // Argument matcher which matches that the state is equal even if the package names are out of + // order (since they are logically a set). + public static final class UidPackageStateMatcher implements ArgumentMatcher<UidPackageState> { + private final int mUid; + private final List<String> mSortedPackages; + + public UidPackageStateMatcher(int uid, List<String> packageNames) { + mUid = uid; + if (packageNames != null) { + mSortedPackages = new ArrayList(packageNames); + Collections.sort(mSortedPackages); + } else { + mSortedPackages = null; + } + } + + public UidPackageStateMatcher(UidPackageState toMatch) { + this(toMatch.uid, toMatch.packageNames); + } + + @Override + public boolean matches(UidPackageState state) { + if (state == null) return false; + if (state.uid != mUid) return false; + if ((state.packageNames == null) != (mSortedPackages == null)) return false; + var copy = new ArrayList(state.packageNames); + Collections.sort(copy); + return mSortedPackages.equals(copy); + } + + @Override + public String toString() { + return "Matcher for UidState with uid: " + mUid + ": " + mSortedPackages; + } + } + + public static final class PackageStateListMatcher + implements ArgumentMatcher<List<UidPackageState>> { + + private final List<UidPackageState> mToMatch; + + public PackageStateListMatcher(List<UidPackageState> toMatch) { + mToMatch = Objects.requireNonNull(toMatch); + } + + @Override + public boolean matches(List<UidPackageState> other) { + if (other == null) return false; + if (other.size() != mToMatch.size()) return false; + for (int i = 0; i < mToMatch.size(); i++) { + var matcher = new UidPackageStateMatcher(mToMatch.get(i)); + if (!matcher.matches(other.get(i))) return false; + } + return true; + } + + @Override + public String toString() { + return "Matcher for List<UidState> with uid: " + mToMatch; + } + } + + @Before + public void setup() { + when(mMockPackageStateOne_10000_one.getAppId()).thenReturn(10000); + when(mMockPackageStateOne_10000_one.getPackageName()).thenReturn("com.package.one"); + + when(mMockPackageStateTwo_10001_two.getAppId()).thenReturn(10001); + when(mMockPackageStateTwo_10001_two.getPackageName()).thenReturn("com.package.two"); + + // Same state as the first is intentional, emulating multi-user + when(mMockPackageStateThree_10000_one.getAppId()).thenReturn(10000); + when(mMockPackageStateThree_10000_one.getPackageName()).thenReturn("com.package.one"); + + when(mMockPackageStateFour_10000_three.getAppId()).thenReturn(10000); + when(mMockPackageStateFour_10000_three.getPackageName()).thenReturn("com.package.three"); + + when(mMockPackageStateFive_10001_four.getAppId()).thenReturn(10001); + when(mMockPackageStateFive_10001_four.getPackageName()).thenReturn("com.package.four"); + + when(mMockPackageStateSix_10000_two.getAppId()).thenReturn(10000); + when(mMockPackageStateSix_10000_two.getPackageName()).thenReturn("com.package.two"); + } + + @Test + public void testInitialPackagePopulation() throws Exception { + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateThree_10000_one, + mMockPackageStateFour_10000_three, + mMockPackageStateFive_10001_four, + mMockPackageStateSix_10000_two); + var expectedPackageList = + List.of( + createUidPackageState( + 10000, + List.of("com.package.one", "com.package.two", "com.package.three")), + createUidPackageState( + 10001, List.of("com.package.two", "com.package.four"))); + + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + verify(mMockPc) + .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList))); + } + + @Test + public void testOnModifyPackageState_whenNewUid() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // new uid, including user component + mPermissionProvider.onModifyPackageState(1_10002, "com.package.new", false /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat(new UidPackageStateMatcher(10002, List.of("com.package.new")))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenRemoveUid() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + + verify(mMockPc).updatePackagesForUid(argThat(new UidPackageStateMatcher(10000, List.of()))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenUpdatedUidAddition() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.new", false /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10000, List.of("com.package.one", "com.package.new")))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenUpdateUidRemoval() throws Exception { + // 10000: one, two | 10001: two + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateSix_10000_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + createUidPackageState(10000, List.of("com.package.two"))))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnServiceStart() throws Exception { + // 10000: one, two | 10001: two + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateSix_10000_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + verify(mMockPc) + .updatePackagesForUid( + argThat(new UidPackageStateMatcher(10000, List.of("com.package.two")))); + + verify(mMockPc).updatePackagesForUid(any()); // exactly once + mPermissionProvider.onModifyPackageState( + 1_10000, "com.package.three", false /* isRemove */); + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10000, List.of("com.package.two", "com.package.three")))); + verify(mMockPc, times(2)).updatePackagesForUid(any()); // exactly twice + // state is now 10000: two, three | 10001: two + + // simulate restart of the service + mPermissionProvider.onServiceStart(null); // should handle null + var newMockPc = mock(INativePermissionController.class); + mPermissionProvider.onServiceStart(newMockPc); + + var expectedPackageList = + List.of( + createUidPackageState( + 10000, List.of("com.package.two", "com.package.three")), + createUidPackageState(10001, List.of("com.package.two"))); + + verify(newMockPc) + .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList))); + + verify(newMockPc, never()).updatePackagesForUid(any()); + // updates should still work after restart + mPermissionProvider.onModifyPackageState(10001, "com.package.four", false /* isRemove */); + verify(newMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10001, List.of("com.package.two", "com.package.four")))); + // exactly once + verify(newMockPc).updatePackagesForUid(any()); + } + + private static UidPackageState createUidPackageState(int uid, List<String> packages) { + var res = new UidPackageState(); + res.uid = uid; + res.packageNames = packages; + return res; + } +} diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java index 634877eb2539..037c3c00443c 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java @@ -66,6 +66,7 @@ public class AudioServiceTest { @Mock private AppOpsManager mMockAppOpsManager; @Mock private AudioPolicyFacade mMockAudioPolicy; @Mock private PermissionEnforcer mMockPermissionEnforcer; + @Mock private AudioServerPermissionProvider mMockPermissionProvider; // the class being unit-tested here private AudioService mAudioService; @@ -86,7 +87,7 @@ public class AudioServiceTest { .thenReturn(AppOpsManager.MODE_ALLOWED); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null, - mMockAppOpsManager, mMockPermissionEnforcer); + mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider); } /** diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java index 8dfcc1843fed..27b552fa7cdd 100644 --- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java @@ -22,11 +22,13 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.mock; import android.annotation.NonNull; +import android.app.AppOpsManager; import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.IDeviceVolumeBehaviorDispatcher; +import android.os.PermissionEnforcer; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -75,7 +77,8 @@ public class DeviceVolumeBehaviorTest { mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase(); mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, - mTestLooper.getLooper()); + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)); mTestLooper.dispatchAll(); } diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index 23728db34c34..8e34ee1b6a42 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -40,6 +40,7 @@ import static android.view.KeyEvent.ACTION_DOWN; import static android.view.KeyEvent.KEYCODE_VOLUME_UP; import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME; +import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -132,9 +133,12 @@ public class VolumeHelperTest { @Mock private PermissionEnforcer mMockPermissionEnforcer; @Mock + private AudioServerPermissionProvider mMockPermissionProvider; + @Mock private AudioVolumeGroupHelperBase mAudioVolumeGroupHelper; - private final AudioPolicyFacade mFakeAudioPolicy = lookbackAudio -> false; + @Mock + private AudioPolicyFacade mMockAudioPolicy; private AudioVolumeGroup mAudioMusicVolumeGroup; @@ -153,9 +157,10 @@ public class VolumeHelperTest { SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, @Nullable Looper looper, AppOpsManager appOps, - @NonNull PermissionEnforcer enforcer) { + @NonNull PermissionEnforcer enforcer, + AudioServerPermissionProvider permissionProvider) { super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, appOps, enforcer); + audioPolicy, looper, appOps, enforcer, permissionProvider); } public void setDeviceForStream(int stream, int device) { @@ -209,8 +214,9 @@ public class VolumeHelperTest { mAm = mContext.getSystemService(AudioManager.class); mAudioService = new MyAudioService(mContext, mSpyAudioSystem, mSpySystemServer, - mSettingsAdapter, mAudioVolumeGroupHelper, mFakeAudioPolicy, - mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer); + mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, + mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer, + mMockPermissionProvider); mTestLooper.dispatchAll(); prepareAudioServiceState(); @@ -552,7 +558,7 @@ public class VolumeHelperTest { } @Test - @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME) + @RequiresFlagsDisabled({FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME, FLAG_ABS_VOLUME_INDEX_FIX}) public void configurablePreScaleAbsoluteVolume_checkIndex() throws Exception { final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC); final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC); @@ -607,6 +613,7 @@ public class VolumeHelperTest { @Test @RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME) + @RequiresFlagsDisabled(FLAG_ABS_VOLUME_INDEX_FIX) public void disablePreScaleAbsoluteVolume_checkIndex() throws Exception { final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC); final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 4a9760bc3317..e91fd3794a48 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2726,6 +2726,19 @@ public class ActivityRecordTests extends WindowTestsBase { assertNoStartingWindow(activity); } + @Test + public void testPostCleanupStartingWindow() { + registerTestStartingWindowOrganizer(); + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + activity.addStartingWindow(mPackageName, android.R.style.Theme, null, true, true, false, + true, false, false, false); + waitUntilHandlersIdle(); + assertHasStartingWindow(activity); + // Simulate Shell remove starting window actively. + activity.mStartingWindow.removeImmediately(); + assertNoStartingWindow(activity); + } + private void testLegacySplashScreen(int targetSdk, int verifyType) { final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); activity.mTargetSdk = targetSdk; diff --git a/telephony/java/android/telephony/CarrierRestrictionRules.java b/telephony/java/android/telephony/CarrierRestrictionRules.java index d5db61233230..65e8e13036a6 100644 --- a/telephony/java/android/telephony/CarrierRestrictionRules.java +++ b/telephony/java/android/telephony/CarrierRestrictionRules.java @@ -555,10 +555,11 @@ public final class CarrierRestrictionRules implements Parcelable { * Set the device's carrier restriction status * * @param carrierRestrictionStatus device restriction status - * @hide */ public @NonNull - Builder setCarrierRestrictionStatus(int carrierRestrictionStatus) { + @FlaggedApi(Flags.FLAG_SET_CARRIER_RESTRICTION_STATUS) + Builder setCarrierRestrictionStatus( + @CarrierRestrictionStatus int carrierRestrictionStatus) { mRules.mCarrierRestrictionStatus = carrierRestrictionStatus; return this; } diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt index dc5013519dbf..ed6e8df3e293 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt @@ -23,6 +23,7 @@ import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.traces.component.ComponentNameMatcher +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import org.junit.FixMethodOrder @@ -77,6 +78,7 @@ class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: LegacyFlickerTest) : @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible() + @FlakyTest(bugId = 330486656) @Presubmit @Test fun imeAppLayerIsAlwaysVisible() { diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index a8b383cd4274..093923f3ed53 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -101,8 +101,8 @@ public class PackageWatchdogTest { private static final String OBSERVER_NAME_2 = "observer2"; private static final String OBSERVER_NAME_3 = "observer3"; private static final String OBSERVER_NAME_4 = "observer4"; - private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(10); - private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(50); + private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1); + private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -1453,8 +1453,7 @@ public class PackageWatchdogTest { raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); - moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS - - TimeUnit.MINUTES.toMillis(1)); + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS); // The first failure will be outside the threshold. raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, @@ -1713,9 +1712,6 @@ public class PackageWatchdogTest { watchdog.onPackageFailure(packages, failureReason); } mTestLooper.dispatchAll(); - if (Flags.recoverabilityDetection()) { - moveTimeForwardAndDispatch(watchdog.DEFAULT_MITIGATION_WINDOW_MS); - } } private PackageWatchdog createWatchdog() { diff --git a/tests/TrustTests/AndroidManifest.xml b/tests/TrustTests/AndroidManifest.xml index 30cf345db34d..2f6c0dd14adc 100644 --- a/tests/TrustTests/AndroidManifest.xml +++ b/tests/TrustTests/AndroidManifest.xml @@ -78,6 +78,7 @@ <action android:name="android.service.trust.TrustAgentService" /> </intent-filter> </service> + <service android:name=".IsActiveUnlockRunningTrustAgent" android:exported="true" @@ -88,6 +89,16 @@ </intent-filter> </service> + <service + android:name=".UnlockAttemptTrustAgent" + android:exported="true" + android:label="Test Agent" + android:permission="android.permission.BIND_TRUST_AGENT"> + <intent-filter> + <action android:name="android.service.trust.TrustAgentService" /> + </intent-filter> + </service> + </application> <!-- self-instrumenting test package. --> diff --git a/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt new file mode 100644 index 000000000000..2c9361df63fd --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.trust.test + +import android.app.trust.TrustManager +import android.content.Context +import android.trust.BaseTrustAgentService +import android.trust.TrustTestActivity +import android.trust.test.lib.LockStateTrackingRule +import android.trust.test.lib.ScreenLockRule +import android.trust.test.lib.TestTrustListener +import android.trust.test.lib.TrustAgentRule +import android.util.Log +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +/** + * Test for the impacts of reporting unlock attempts. + * + * atest TrustTests:UnlockAttemptTest + */ +@RunWith(AndroidJUnit4::class) +class UnlockAttemptTest { + private val context = getApplicationContext<Context>() + private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager + private val userId = context.userId + private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java) + private val screenLockRule = ScreenLockRule(requireStrongAuth = true) + private val lockStateTrackingRule = LockStateTrackingRule() + private val trustAgentRule = + TrustAgentRule<UnlockAttemptTrustAgent>(startUnlocked = false, startEnabled = false) + + private val trustListener = UnlockAttemptTrustListener() + private val agent get() = trustAgentRule.agent + + @get:Rule + val rule: RuleChain = + RuleChain.outerRule(activityScenarioRule) + .around(screenLockRule) + .around(lockStateTrackingRule) + .around(trustAgentRule) + + @Before + fun setUp() { + trustManager.registerTrustListener(trustListener) + } + + @Test + fun successfulUnlockAttempt_allowsTrustAgentToStart() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) { + trustAgentRule.enableTrustAgent() + + triggerSuccessfulUnlock() + + trustAgentRule.verifyAgentIsRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START) + } + + @Test + fun successfulUnlockAttempt_notifiesTrustAgent() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldSuccessfulCount = agent.successfulUnlockCallCount + val oldFailedCount = agent.failedUnlockCallCount + + triggerSuccessfulUnlock() + + assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount + 1) + assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount) + } + + @Test + fun successfulUnlockAttempt_notifiesTrustListenerOfManagedTrust() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0 + + triggerSuccessfulUnlock() + + assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo( + oldTrustManagedChangedCount + 1 + ) + } + + @Test + fun failedUnlockAttempt_doesNotAllowTrustAgentToStart() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) { + trustAgentRule.enableTrustAgent() + + triggerFailedUnlock() + + trustAgentRule.ensureAgentIsNotRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START) + } + + @Test + fun failedUnlockAttempt_notifiesTrustAgent() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldSuccessfulCount = agent.successfulUnlockCallCount + val oldFailedCount = agent.failedUnlockCallCount + + triggerFailedUnlock() + + assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount) + assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount + 1) + } + + @Test + fun failedUnlockAttempt_doesNotNotifyTrustListenerOfManagedTrust() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0 + + triggerFailedUnlock() + + assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo( + oldTrustManagedChangedCount + ) + } + + private fun runUnlockAttemptTest( + enableAndVerifyTrustAgent: Boolean, + managingTrust: Boolean, + testBlock: () -> Unit, + ) { + if (enableAndVerifyTrustAgent) { + Log.i(TAG, "Triggering successful unlock") + triggerSuccessfulUnlock() + Log.i(TAG, "Enabling and waiting for trust agent") + trustAgentRule.enableAndVerifyTrustAgentIsRunning( + MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START + ) + Log.i(TAG, "Managing trust: $managingTrust") + agent.setManagingTrust(managingTrust) + await() + } + testBlock() + } + + private fun triggerSuccessfulUnlock() { + screenLockRule.successfulScreenLockAttempt() + trustAgentRule.reportSuccessfulUnlock() + await() + } + + private fun triggerFailedUnlock() { + screenLockRule.failedScreenLockAttempt() + trustAgentRule.reportFailedUnlock() + await() + } + + companion object { + private const val TAG = "UnlockAttemptTest" + private fun await(millis: Long = 500) = Thread.sleep(millis) + private const val MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START = 10000L + } +} + +class UnlockAttemptTrustAgent : BaseTrustAgentService() { + var successfulUnlockCallCount: Long = 0 + private set + var failedUnlockCallCount: Long = 0 + private set + + override fun onUnlockAttempt(successful: Boolean) { + super.onUnlockAttempt(successful) + if (successful) { + successfulUnlockCallCount++ + } else { + failedUnlockCallCount++ + } + } +} + +private class UnlockAttemptTrustListener : TestTrustListener() { + var enabledTrustAgentsChangedCount = mutableMapOf<Int, Int>() + var onTrustManagedChangedCount = mutableMapOf<Int, Int>() + + override fun onEnabledTrustAgentsChanged(userId: Int) { + enabledTrustAgentsChangedCount.compute(userId) { _: Int, curr: Int? -> + if (curr == null) 0 else curr + 1 + } + } + + data class TrustChangedParams( + val enabled: Boolean, + val newlyUnlocked: Boolean, + val userId: Int, + val flags: Int, + val trustGrantedMessages: MutableList<String>? + ) + + val onTrustChangedCalls = mutableListOf<TrustChangedParams>() + + override fun onTrustChanged( + enabled: Boolean, + newlyUnlocked: Boolean, + userId: Int, + flags: Int, + trustGrantedMessages: MutableList<String> + ) { + onTrustChangedCalls += TrustChangedParams( + enabled, newlyUnlocked, userId, flags, trustGrantedMessages + ) + } + + override fun onTrustManagedChanged(enabled: Boolean, userId: Int) { + onTrustManagedChangedCount.compute(userId) { _: Int, curr: Int? -> + if (curr == null) 0 else curr + 1 + } + } +} diff --git a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt index f1edca3ff86e..1ccdcc623c5b 100644 --- a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt +++ b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt @@ -24,6 +24,8 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.internal.widget.LockscreenCredential import com.google.common.truth.Truth.assertWithMessage import org.junit.rules.TestRule @@ -32,13 +34,18 @@ import org.junit.runners.model.Statement /** * Sets a screen lock on the device for the duration of the test. + * + * @param requireStrongAuth Whether a strong auth is required at the beginning. + * If true, trust agents will not be available until the user verifies their credentials. */ -class ScreenLockRule : TestRule { +class ScreenLockRule(val requireStrongAuth: Boolean = false) : TestRule { private val context: Context = getApplicationContext() + private val userId = context.userId private val uiDevice = UiDevice.getInstance(getInstrumentation()) private val windowManager = checkNotNull(WindowManagerGlobal.getWindowManagerService()) private val lockPatternUtils = LockPatternUtils(context) private var instantLockSavedValue = false + private var strongAuthSavedValue: Int = 0 override fun apply(base: Statement, description: Description) = object : Statement() { override fun evaluate() { @@ -46,10 +53,12 @@ class ScreenLockRule : TestRule { dismissKeyguard() setScreenLock() setLockOnPowerButton() + configureStrongAuthState() try { base.evaluate() } finally { + restoreStrongAuthState() removeScreenLock() revertLockOnPowerButton() dismissKeyguard() @@ -57,6 +66,22 @@ class ScreenLockRule : TestRule { } } + private fun configureStrongAuthState() { + strongAuthSavedValue = lockPatternUtils.getStrongAuthForUser(userId) + if (requireStrongAuth) { + Log.d(TAG, "Triggering strong auth due to simulated lockdown") + lockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, userId) + wait("strong auth required after lockdown") { + lockPatternUtils.getStrongAuthForUser(userId) == + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN + } + } + } + + private fun restoreStrongAuthState() { + lockPatternUtils.requireStrongAuth(strongAuthSavedValue, userId) + } + private fun verifyNoScreenLockAlreadySet() { assertWithMessage("Screen Lock must not already be set on device") .that(lockPatternUtils.isSecure(context.userId)) @@ -82,6 +107,22 @@ class ScreenLockRule : TestRule { } } + fun successfulScreenLockAttempt() { + lockPatternUtils.verifyCredential(LockscreenCredential.createPin(PIN), context.userId, 0) + lockPatternUtils.userPresent(context.userId) + wait("strong auth not required") { + lockPatternUtils.getStrongAuthForUser(context.userId) == STRONG_AUTH_NOT_REQUIRED + } + } + + fun failedScreenLockAttempt() { + lockPatternUtils.verifyCredential( + LockscreenCredential.createPin(WRONG_PIN), + context.userId, + 0 + ) + } + private fun setScreenLock() { lockPatternUtils.setLockCredential( LockscreenCredential.createPin(PIN), @@ -121,5 +162,6 @@ class ScreenLockRule : TestRule { companion object { private const val TAG = "ScreenLockRule" private const val PIN = "0000" + private const val WRONG_PIN = "0001" } } diff --git a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt index 18bc029b6845..404c6d968b3a 100644 --- a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt +++ b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt @@ -20,14 +20,15 @@ import android.app.trust.TrustManager import android.content.ComponentName import android.content.Context import android.trust.BaseTrustAgentService +import android.trust.test.lib.TrustAgentRule.Companion.invoke import android.util.Log import androidx.test.core.app.ApplicationProvider.getApplicationContext import com.android.internal.widget.LockPatternUtils import com.google.common.truth.Truth.assertWithMessage +import kotlin.reflect.KClass import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import kotlin.reflect.KClass /** * Enables a trust agent and causes the system service to bind to it. @@ -37,7 +38,9 @@ import kotlin.reflect.KClass * @constructor Creates the rule. Do not use; instead, use [invoke]. */ class TrustAgentRule<T : BaseTrustAgentService>( - private val serviceClass: KClass<T> + private val serviceClass: KClass<T>, + private val startUnlocked: Boolean, + private val startEnabled: Boolean, ) : TestRule { private val context: Context = getApplicationContext() private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager @@ -48,11 +51,18 @@ class TrustAgentRule<T : BaseTrustAgentService>( override fun apply(base: Statement, description: Description) = object : Statement() { override fun evaluate() { verifyTrustServiceRunning() - unlockDeviceWithCredential() - enableTrustAgent() + if (startUnlocked) { + reportSuccessfulUnlock() + } else { + Log.i(TAG, "Trust manager not starting in unlocked state") + } try { - verifyAgentIsRunning() + if (startEnabled) { + enableAndVerifyTrustAgentIsRunning() + } else { + Log.i(TAG, "Trust agent ${serviceClass.simpleName} not enabled") + } base.evaluate() } finally { disableTrustAgent() @@ -64,12 +74,22 @@ class TrustAgentRule<T : BaseTrustAgentService>( assertWithMessage("Trust service is not running").that(trustManager).isNotNull() } - private fun unlockDeviceWithCredential() { - Log.d(TAG, "Unlocking device with credential") + fun reportSuccessfulUnlock() { + Log.i(TAG, "Reporting successful unlock") trustManager.reportUnlockAttempt(true, context.userId) } - private fun enableTrustAgent() { + fun reportFailedUnlock() { + Log.i(TAG, "Reporting failed unlock") + trustManager.reportUnlockAttempt(false, context.userId) + } + + fun enableAndVerifyTrustAgentIsRunning(maxWait: Long = 30000L) { + enableTrustAgent() + verifyAgentIsRunning(maxWait) + } + + fun enableTrustAgent() { val componentName = ComponentName(context, serviceClass.java) val userId = context.userId Log.i(TAG, "Enabling trust agent ${componentName.flattenToString()} for user $userId") @@ -79,12 +99,18 @@ class TrustAgentRule<T : BaseTrustAgentService>( lockPatternUtils.setEnabledTrustAgents(agents, userId) } - private fun verifyAgentIsRunning() { - wait("${serviceClass.simpleName} to be running") { + fun verifyAgentIsRunning(maxWait: Long = 30000L) { + wait("${serviceClass.simpleName} to be running", maxWait) { BaseTrustAgentService.instance(serviceClass) != null } } + fun ensureAgentIsNotRunning(window: Long = 30000L) { + ensure("${serviceClass.simpleName} is not running", window) { + BaseTrustAgentService.instance(serviceClass) == null + } + } + private fun disableTrustAgent() { val componentName = ComponentName(context, serviceClass.java) val userId = context.userId @@ -97,13 +123,23 @@ class TrustAgentRule<T : BaseTrustAgentService>( companion object { /** - * Creates a new rule for the specified agent class. Example usage: + * Creates a new rule for the specified agent class. Starts with the device unlocked and + * the trust agent enabled. Example usage: * ``` * @get:Rule val rule = TrustAgentRule<MyTestAgent>() * ``` + * + * Also supports setting different device lock and trust agent enablement states: + * ``` + * @get:Rule val rule = TrustAgentRule<MyTestAgent>(startUnlocked = false, startEnabled = false) + * ``` */ - inline operator fun <reified T : BaseTrustAgentService> invoke() = - TrustAgentRule(T::class) + inline operator fun <reified T : BaseTrustAgentService> invoke( + startUnlocked: Boolean = true, + startEnabled: Boolean = true, + ) = + TrustAgentRule(T::class, startUnlocked, startEnabled) + private const val TAG = "TrustAgentRule" } diff --git a/tests/TrustTests/src/android/trust/test/lib/utils.kt b/tests/TrustTests/src/android/trust/test/lib/Utils.kt index e047202f6740..3b32b47a6160 100644 --- a/tests/TrustTests/src/android/trust/test/lib/utils.kt +++ b/tests/TrustTests/src/android/trust/test/lib/Utils.kt @@ -39,7 +39,7 @@ internal fun wait( ) { var waited = 0L var count = 0 - while (!conditionFunction.invoke(count)) { + while (!conditionFunction(count)) { assertWithMessage("Condition exceeded maximum wait time of $maxWait ms: $description") .that(waited <= maxWait) .isTrue() @@ -49,3 +49,34 @@ internal fun wait( Thread.sleep(rate) } } + +/** + * Ensures that [conditionFunction] is true with a failed assertion if it is not within [window] + * ms. + * + * The condition function can perform additional logic (for example, logging or attempting to make + * the condition become true). + * + * @param conditionFunction function which takes the attempt count & returns whether the condition + * is met + */ +internal fun ensure( + description: String? = null, + window: Long = 30000L, + rate: Long = 50L, + conditionFunction: (count: Int) -> Boolean +) { + var waited = 0L + var count = 0 + while (waited <= window) { + assertWithMessage("Condition failed within $window ms: $description").that( + conditionFunction( + count + ) + ).isTrue() + waited += rate + count++ + Log.i(TAG, "Ensuring $description ($waited/$window) #$count") + Thread.sleep(rate) + } +} |