diff options
49 files changed, 814 insertions, 497 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index e9a63f74d59f..17e7d7a258d8 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -19346,7 +19346,7 @@ package android.hardware.biometrics { public class BiometricManager { method @Deprecated @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public int canAuthenticate(); method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public int canAuthenticate(int); - method @FlaggedApi("android.hardware.biometrics.last_authentication_time") @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public long getLastAuthenticationTime(int); + method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public long getLastAuthenticationTime(int); method @NonNull @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public android.hardware.biometrics.BiometricManager.Strings getStrings(int); field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1 field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE = 20; // 0x14 @@ -19354,7 +19354,7 @@ package android.hardware.biometrics { field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21; // 0x15 field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf - field @FlaggedApi("android.hardware.biometrics.last_authentication_time") public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL + field public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL field public static final int BIOMETRIC_SUCCESS = 0; // 0x0 } @@ -19407,7 +19407,7 @@ package android.hardware.biometrics { field public static final int BIOMETRIC_ERROR_UNABLE_TO_PROCESS = 2; // 0x2 field public static final int BIOMETRIC_ERROR_USER_CANCELED = 10; // 0xa field public static final int BIOMETRIC_ERROR_VENDOR = 8; // 0x8 - field @FlaggedApi("android.hardware.biometrics.last_authentication_time") public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL + field public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL } public abstract static class BiometricPrompt.AuthenticationCallback { @@ -21750,6 +21750,7 @@ package android.media { field public static final int CHANNEL_IN_X_AXIS = 2048; // 0x800 field public static final int CHANNEL_IN_Y_AXIS = 4096; // 0x1000 field public static final int CHANNEL_IN_Z_AXIS = 8192; // 0x2000 + field @FlaggedApi("android.media.audio.sony_360ra_mpegh_3d_format") public static final int CHANNEL_OUT_13POINT0 = 30136348; // 0x1cbd81c field public static final int CHANNEL_OUT_5POINT1 = 252; // 0xfc field public static final int CHANNEL_OUT_5POINT1POINT2 = 3145980; // 0x3000fc field public static final int CHANNEL_OUT_5POINT1POINT4 = 737532; // 0xb40fc diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bfbdb7276be0..41f286245d8d 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -16042,7 +16042,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isAnyRadioPoweredOn(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isApnMetered(int); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isApplicationOnUicc(int); - method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isCellularIdentifierDisclosureNotificationsEnabled(); + method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isCellularIdentifierDisclosureNotificationsEnabled(); method public boolean isDataConnectivityPossible(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isDataEnabledForApn(int); method @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isDomainSelectionSupported(); @@ -16093,7 +16093,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataActivationState(int); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataRoamingEnabled(boolean); - method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean); + method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult setIccLockEnabled(boolean, @NonNull String); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 7b9ec4a7821e..2d7ed46fe64a 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -3959,10 +3959,20 @@ public final class ActivityThread extends ClientTransactionHandler /** Converts a process state to a VM process state. */ private static int toVmProcessState(int processState) { - final int state = ActivityManager.isProcStateJankPerceptible(processState) - ? VM_PROCESS_STATE_JANK_PERCEPTIBLE - : VM_PROCESS_STATE_JANK_IMPERCEPTIBLE; - return state; + if (ActivityManager.isProcStateJankPerceptible(processState)) { + return VM_PROCESS_STATE_JANK_PERCEPTIBLE; + } + + if (Flags.jankPerceptibleNarrow()) { + // Unlike other persistent processes, system server is often on + // the critical path for application startup. Mark it explicitly + // as jank perceptible regardless of processState. + if (isSystem()) { + return VM_PROCESS_STATE_JANK_PERCEPTIBLE; + } + } + + return VM_PROCESS_STATE_JANK_IMPERCEPTIBLE; } /** Update VM state based on ActivityManager.PROCESS_STATE_* constants. */ diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 8021ab4865af..ba8fbc121e8d 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -128,7 +128,7 @@ public final class UiAutomation { private static final String LOG_TAG = UiAutomation.class.getSimpleName(); private static final boolean DEBUG = false; - private static final boolean VERBOSE = false; + private static final boolean VERBOSE = Build.IS_DEBUGGABLE; private static final int CONNECTION_ID_UNDEFINED = -1; diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java index 875adbdf3913..7dc6afba3f1c 100644 --- a/core/java/android/hardware/biometrics/BiometricConstants.java +++ b/core/java/android/hardware/biometrics/BiometricConstants.java @@ -334,6 +334,5 @@ public interface BiometricConstants { * Returned from {@link BiometricManager#getLastAuthenticationTime(int)} when there has * been no successful authentication for the given authenticator since boot. */ - @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) long BIOMETRIC_NO_AUTHENTICATION = -1; } diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index c690c67ed79f..cefe20c15ced 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -116,7 +116,6 @@ public class BiometricManager { * Returned from {@link BiometricManager#getLastAuthenticationTime(int)} when no matching * successful authentication has been performed since boot. */ - @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) public static final long BIOMETRIC_NO_AUTHENTICATION = BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; @@ -777,7 +776,6 @@ public class BiometricManager { */ @RequiresPermission(USE_BIOMETRIC) @ElapsedRealtimeLong - @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) public long getLastAuthenticationTime( @BiometricManager.Authenticators.Types int authenticators) { if (authenticators == 0 diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index 73b6417a6ba4..4815f3e4f524 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -2,14 +2,6 @@ package: "android.hardware.biometrics" container: "system" flag { - name: "last_authentication_time" - is_exported: true - namespace: "wallet_integration" - description: "Feature flag for adding getLastAuthenticationTime API to BiometricManager" - bug: "301979982" -} - -flag { name: "add_key_agreement_crypto_object" is_exported: true namespace: "biometrics" diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index d451109554fa..ddfa3799706e 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -84,17 +84,8 @@ public class TestLooperManager { * interactions with it have completed. */ public Message next() { - // Wait for the looper block to come up, to make sure we don't accidentally get - // the message for the block. - while (!mLooperIsMyLooper && !mLooperBlocked) { - synchronized (this) { - try { - wait(); - } catch (InterruptedException e) { - } - } - } checkReleased(); + waitForLooperHolder(); return mQueue.next(); } @@ -110,6 +101,7 @@ public class TestLooperManager { @Nullable public Message poll() { checkReleased(); + waitForLooperHolder(); return mQueue.pollForTest(); } @@ -124,6 +116,7 @@ public class TestLooperManager { @Nullable public Long peekWhen() { checkReleased(); + waitForLooperHolder(); return mQueue.peekWhenForTest(); } @@ -133,6 +126,7 @@ public class TestLooperManager { @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY) public boolean isBlockedOnSyncBarrier() { checkReleased(); + waitForLooperHolder(); return mQueue.isBlockedOnSyncBarrier(); } @@ -221,6 +215,23 @@ public class TestLooperManager { } } + /** + * Waits until the Looper is blocked by the LooperHolder, if one was posted. + * + * After this method returns, it's guaranteed that the LooperHolder Message + * is not in the underlying queue. + */ + private void waitForLooperHolder() { + while (!mLooperIsMyLooper && !mLooperBlocked) { + synchronized (this) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + } + private class LooperHolder implements Runnable { @Override public void run() { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index cd8a85a66c1a..bf34069f9445 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -5566,9 +5566,6 @@ public final class ViewRootImpl implements ViewParent, if (mAttachInfo.mContentCaptureManager != null) { ContentCaptureSession session = mAttachInfo.mContentCaptureManager.getMainContentCaptureSession(); - if (android.view.contentcapture.flags.Flags.postCreateAndroidBgThread()) { - session.performStart(); - } session.notifyWindowBoundsChanged(session.getId(), getConfiguration().windowConfiguration.getBounds()); } diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 3f3484d5a527..724e8fa830af 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -52,6 +52,7 @@ import android.view.contentcapture.ContentCaptureSession.FlushReason; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; import com.android.internal.util.RingBuffer; import com.android.internal.util.SyncResultReceiver; @@ -604,6 +605,7 @@ public final class ContentCaptureManager { mContext, this, prepareUiHandler(), + prepareContentCaptureHandler(), mService ); if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession); @@ -614,6 +616,15 @@ public final class ContentCaptureManager { @NonNull @GuardedBy("mLock") + private Handler prepareContentCaptureHandler() { + if (mContentCaptureHandler == null) { + mContentCaptureHandler = BackgroundThread.getHandler(); + } + return mContentCaptureHandler; + } + + @NonNull + @GuardedBy("mLock") private Handler prepareUiHandler() { if (mUiHandler == null) { mUiHandler = Handler.createAsync(Looper.getMainLooper()); diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java index 6bb2975d9cf1..9aeec20ec9b7 100644 --- a/core/java/android/view/contentcapture/ContentCaptureSession.java +++ b/core/java/android/view/contentcapture/ContentCaptureSession.java @@ -286,9 +286,6 @@ public abstract class ContentCaptureSession implements AutoCloseable { abstract void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags); - /** @hide */ - public void performStart() {} - abstract boolean isDisabled(); /** diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index eddfc42da9bd..2fb78c038ca2 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -57,12 +57,10 @@ import android.view.View; import android.view.ViewStructure; import android.view.autofill.AutofillId; import android.view.contentcapture.ViewNode.ViewStructureImpl; -import android.view.contentcapture.flags.Flags; import android.view.contentprotection.ContentProtectionEventProcessor; import android.view.inputmethod.BaseInputConnection; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.os.BackgroundThread; import com.android.internal.os.IResultReceiver; import com.android.modules.expresslog.Counter; @@ -109,10 +107,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { @NonNull private final Handler mUiHandler; - /** @hide */ - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) - @Nullable - public Handler mContentCaptureHandler; + @NonNull + private final Handler mContentCaptureHandler; /** * Interface to the system_server binder object - it's only used to start the session (and @@ -191,12 +187,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { @Nullable public ContentProtectionEventProcessor mContentProtectionEventProcessor; - /** - * A runnable object to perform the start of this session. - */ - @Nullable - private Runnable mStartRunnable = null; - private static class SessionStateReceiver extends IResultReceiver.Stub { private final WeakReference<MainContentCaptureSession> mMainSession; @@ -208,7 +198,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { public void send(int resultCode, Bundle resultData) { final MainContentCaptureSession mainSession = mMainSession.get(); if (mainSession == null) { - Log.w(TAG, "received result after main session released"); + Log.w(TAG, "received result after mina session released"); return; } final IBinder binder; @@ -223,8 +213,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { binder = resultData.getBinder(EXTRA_BINDER); if (binder == null) { Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result"); - // explicitly init the bg thread - mainSession.mContentCaptureHandler = mainSession.prepareContentCaptureHandler(); mainSession.runOnContentCaptureThread(() -> mainSession.resetSession( STATE_DISABLED | STATE_INTERNAL_ERROR)); return; @@ -232,45 +220,23 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } else { binder = null; } - // explicitly init the bg thread - mainSession.mContentCaptureHandler = mainSession.prepareContentCaptureHandler(); mainSession.runOnContentCaptureThread(() -> mainSession.onSessionStarted(resultCode, binder)); } } - /** - * Prepares the content capture handler(i.e. the background thread). - * - * This is expected to be called from the {@link SessionStateReceiver#send} callback, after the - * session {@link performStart}. This is expected to be executed in a binder thread, instead - * of the UI thread. - */ - @NonNull - private Handler prepareContentCaptureHandler() { - if (mContentCaptureHandler == null) { - try { - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "prepareContentCaptureHandler"); - } - mContentCaptureHandler = BackgroundThread.getHandler(); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - } - return mContentCaptureHandler; - } - /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) public MainContentCaptureSession( @NonNull ContentCaptureManager.StrippedContext context, @NonNull ContentCaptureManager manager, @NonNull Handler uiHandler, + @NonNull Handler contentCaptureHandler, @NonNull IContentCaptureManager systemServerInterface) { mContext = context; mManager = manager; mUiHandler = uiHandler; + mContentCaptureHandler = contentCaptureHandler; mSystemServerInterface = systemServerInterface; final int logHistorySize = mManager.mOptions.logHistorySize; @@ -294,49 +260,18 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } /** - * Performs the start of the session. - * - * This is expected to be called from the UI thread, when the activity finishes its first frame. - * This is a no-op if the session has already been started. - * - * See {@link #start(IBinder, IBinder, ComponentName, int)} for more details. - * - * @hide */ - @Override - public void performStart() { - if (!hasStarted() && mStartRunnable != null) { - mStartRunnable.run(); - } - } - - /** - * Creates a runnable to start this session. - * - * For performance reasons, it is better to only create a task to start the session - * during the creation of the activity and perform the actual start when the activity - * finishes it's first frame. + * Starts this session. */ @Override void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { - if (Flags.postCreateAndroidBgThread()) { - mStartRunnable = () -> { - try { - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "cc session startImpl"); - } - startImpl(token, shareableActivityToken, component, flags); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - }; - } else { - startImpl(token, shareableActivityToken, component, flags); - } + runOnContentCaptureThread( + () -> startImpl(token, shareableActivityToken, component, flags)); } private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { + checkOnContentCaptureThread(); if (!isContentCaptureEnabled()) return; if (sVerbose) { @@ -370,7 +305,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e); } } - @Override void onDestroy() { clearAndRunOnContentCaptureThread(() -> { @@ -627,6 +561,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } private boolean hasStarted() { + checkOnContentCaptureThread(); return mState != UNKNOWN_STATE; } @@ -640,11 +575,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet"); return; } - if (mContentCaptureHandler == null) { - Log.w(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): content capture " - + "thread not ready"); - return; - } if (mDisabled.get()) { // Should not be called on this state, as handleSendEvent checks. @@ -717,11 +647,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { if (!isContentCaptureReceiverEnabled()) { return; } - if (mContentCaptureHandler == null) { - Log.w(TAG, "handleForceFlush(" + getDebugState(reason) + "): content capture thread" - + "not ready"); - return; - } if (mDirectServiceInterface == null) { if (sVerbose) { @@ -838,9 +763,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } mDirectServiceInterface = null; mContentProtectionEventProcessor = null; - if (mContentCaptureHandler != null) { - mContentCaptureHandler.removeMessages(MSG_FLUSH); - } + mContentCaptureHandler.removeMessages(MSG_FLUSH); } @Override @@ -994,10 +917,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * clear the buffer events then starting sending out current event. */ private void enqueueEvent(@NonNull final ContentCaptureEvent event, boolean forceFlush) { - if (mContentCaptureHandler == null) { - mEventProcessQueue.offer(event); - return; - } if (forceFlush || mEventProcessQueue.size() >= mManager.mOptions.maxBufferSize - 1) { // The buffer events are cleared in the same thread first to prevent new events // being added during the time of context switch. This would disrupt the sequence @@ -1200,10 +1119,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * always delegate to the assigned thread from {@code mHandler} for synchronization.</p> */ private void checkOnContentCaptureThread() { - if (mContentCaptureHandler == null) { - Log.e(TAG, "content capture thread is not initiallized!"); - return; - } final boolean onContentCaptureThread = mContentCaptureHandler.getLooper().isCurrentThread(); if (!onContentCaptureThread) { mWrongThreadCount.incrementAndGet(); @@ -1224,12 +1139,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * </p> */ private void runOnContentCaptureThread(@NonNull Runnable r) { - if (mContentCaptureHandler == null) { - Log.e(TAG, "content capture thread is not initiallized!"); - // fall back to UI thread - runOnUiThread(r); - return; - } if (!mContentCaptureHandler.getLooper().isCurrentThread()) { mContentCaptureHandler.post(r); } else { @@ -1238,12 +1147,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) { - if (mContentCaptureHandler == null) { - Log.e(TAG, "content capture thread is not initiallized!"); - // fall back to UI thread - runOnUiThread(r); - return; - } if (!mContentCaptureHandler.getLooper().isCurrentThread()) { mContentCaptureHandler.removeMessages(what); mContentCaptureHandler.post(r); diff --git a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig index 9df835098268..e7bc004ca2d2 100644 --- a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig +++ b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig @@ -15,14 +15,3 @@ flag { bug: "380381249" is_exported: true } - -flag { - name: "post_create_android_bg_thread" - namespace: "pixel_state_server" - description: "Feature flag to post create the bg thread when an app is in the allowlist" - bug: "376468525" - is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } -} diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 222088e8a8b9..51d488fdd76b 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -593,3 +593,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_non_default_display_split" + namespace: "lse_desktop_experience" + description: "Enables split screen on non default displays" + bug: "384999213" +} diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index f9d449cd3b10..4ad6708cda93 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -191,7 +191,7 @@ class FontScaleConverterFactoryTest { .fuzzFractions() .mapNotNull{ FontScaleConverterFactory.forScale(it) } .flatMap{ table -> - generateSequenceOfFractions(-2000f..2000f, step = 0.1f) + generateSequenceOfFractions(-20f..100f, step = 0.1f) .fuzzFractions() .map{ Pair(table, it) } } diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java index 4a5123ec0663..b42bcee77c67 100644 --- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java +++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java @@ -499,57 +499,6 @@ public class MainContentCaptureSessionTest { assertThat(session.mEventProcessQueue).hasSize(1); } - @Test - public void notifyContentCaptureEvents_beforeSessionPerformStart() throws RemoteException { - ContentCaptureOptions options = - createOptions( - /* enableContentCaptureReceiver= */ true, - /* enableContentProtectionReceiver= */ true); - MainContentCaptureSession session = createSession(options); - session.mContentCaptureHandler = null; - session.mDirectServiceInterface = null; - - notifyContentCaptureEvents(session); - mTestableLooper.processAllMessages(); - - assertThat(session.mEvents).isNull(); - assertThat(session.mEventProcessQueue).hasSize(7); // 5 view events + 2 view tree events - } - - @Test - public void notifyViewAppeared_beforeSessionPerformStart() throws RemoteException { - ContentCaptureOptions options = - createOptions( - /* enableContentCaptureReceiver= */ true, - /* enableContentProtectionReceiver= */ true); - MainContentCaptureSession session = createSession(options); - session.mContentCaptureHandler = null; - session.mDirectServiceInterface = null; - - View view = prepareView(session); - session.notifyViewAppeared(session.newViewStructure(view)); - - assertThat(session.mEvents).isNull(); - assertThat(session.mEventProcessQueue).hasSize(1); - } - - @Test - public void flush_beforeSessionPerformStart() throws Exception { - ContentCaptureOptions options = - createOptions( - /* enableContentCaptureReceiver= */ true, - /* enableContentProtectionReceiver= */ true); - MainContentCaptureSession session = createSession(options); - session.mEvents = new ArrayList<>(Arrays.asList(EVENT)); - session.mContentCaptureHandler = null; - session.mDirectServiceInterface = null; - - session.flush(REASON); - - assertThat(session.mEvents).hasSize(1); - assertThat(session.mEventProcessQueue).isEmpty(); - } - /** Simulates the regular content capture events sequence. */ private void notifyContentCaptureEvents(final MainContentCaptureSession session) { final ArrayList<Object> events = new ArrayList<>( @@ -612,8 +561,8 @@ public class MainContentCaptureSessionTest { sStrippedContext, manager, testHandler, + testHandler, mMockSystemServerInterface); - session.mContentCaptureHandler = testHandler; session.mComponentName = COMPONENT_NAME; return session; } diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 065644627393..13d0169c47c5 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -177,10 +177,3 @@ flag { description: "Factor task-view state tracking out of taskviewtransitions" bug: "384976265" } - -flag { - name: "enable_non_default_display_split" - namespace: "multitasking" - description: "Enables split screen on non default displays" - bug: "384999213" -} diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml index debcba071d9c..122cde04f8e4 100644 --- a/libs/WindowManager/Shell/res/values/ids.xml +++ b/libs/WindowManager/Shell/res/values/ids.xml @@ -46,4 +46,9 @@ <item type="id" name="action_move_bubble_bar_right"/> <item type="id" name="dismiss_view"/> + + <!-- Accessibility actions for desktop windowing. --> + <item type="id" name="action_snap_left"/> + <item type="id" name="action_snap_right"/> + <item type="id" name="action_maximize_restore"/> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 468c345259d0..c29b927f61c2 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -333,6 +333,28 @@ <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string> + <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_snap_left">Resize app window left</string> + <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_snap_right">Resize app window right</string> + <!-- Accessibility text for the Maximize Menu's snap maximize/restore [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_maximize_restore">Maximize or restore window size</string> + + <!-- Accessibility action replacement for caption handle menu split screen button [CHAR LIMIT=NONE] --> + <string name="app_handle_menu_talkback_split_screen_mode_button_text">Enter split screen mode</string> + <!-- Accessibility action replacement for caption handle menu enter desktop mode button [CHAR LIMIT=NONE] --> + <string name="app_handle_menu_talkback_desktop_mode_button_text">Enter desktop windowing mode</string> + <!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string> + <!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_snap_right_text">Resize window to right</string> + <!-- Accessibility action replacement for maximize menu enter maximize/restore button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_maximize_restore_text">Maximize or restore window size</string> + <!-- Accessibility action replacement for app header maximize/restore button [CHAR LIMIT=NONE] --> + <string name="maximize_button_talkback_action_maximize_restore_text">Maximize or restore window size</string> + <!-- Accessibility action replacement for app header minimize button [CHAR LIMIT=NONE] --> + <string name="minimize_button_talkback_action_maximize_restore_text">Minimize app window</string> + <!-- Accessibility text for open by default settings button [CHAR LIMIT=NONE] --> <string name="open_by_default_settings_text">Open by default settings</string> <!-- Subheader for open by default menu string. --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index cd5c135691d7..bd89f5cf45f6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -394,11 +394,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Returns the divider position as a fraction from 0 to 1. */ public float getDividerPositionAsFraction() { - return Math.min(1f, Math.max(0f, mIsLeftRightSplit - ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f) - / getBottomRightBounds().right - : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f) - / getBottomRightBounds().bottom)); + if (Flags.enableFlexibleTwoAppSplit()) { + return Math.min(1f, Math.max(0f, mIsLeftRightSplit + ? (getTopLeftBounds().right + getBottomRightBounds().left) / 2f + / getDisplayWidth() + : (getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f + / getDisplayHeight())); + } else { + return Math.min(1f, Math.max(0f, mIsLeftRightSplit + ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f) + / getBottomRightBounds().right + : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f) + / getBottomRightBounds().bottom)); + } } private void updateInvisibleRect() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt index 39dc48d6d206..5d8355625b94 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt @@ -140,6 +140,27 @@ class AppHandleEducationController( windowingEducationViewController.hideEducationTooltip() } } + + // Listens to a [NoCaption] state change to dismiss any tooltip if the app handle or app + // header is gone or de-focused (e.g. when a user swipes up to home, overview, or enters + // split screen) + applicationCoroutineScope.launch { + if ( + isAppHandleHintViewed() && + isEnterDesktopModeHintViewed() && + isExitDesktopModeHintViewed() + ) + return@launch + windowDecorCaptionHandleRepository.captionStateFlow + .filter { captionState -> + captionState is CaptionState.NoCaption && + !isAppHandleHintViewed() && + !isEnterDesktopModeHintViewed() && + !isExitDesktopModeHintViewed() + } + .flowOn(backgroundDispatcher) + .collectLatest { windowingEducationViewController.hideEducationTooltip() } + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 387dbfa807fc..b6765c477485 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -27,18 +27,17 @@ import static android.view.MotionEvent.ACTION_UP; import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION; import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS; - import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode; import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopModeOrShowAppHandle; import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset; -import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener; import android.annotation.NonNull; import android.annotation.Nullable; @@ -112,14 +111,14 @@ import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; -import kotlinx.coroutines.CoroutineScope; -import kotlinx.coroutines.MainCoroutineDispatcher; - import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.MainCoroutineDispatcher; + /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with * {@link DesktopModeWindowDecorViewModel}. @@ -579,6 +578,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeHandleMenu(); closeManageWindowsMenu(); closeMaximizeMenu(); + notifyNoCaptionHandle(); } updateDragResizeListener(oldDecorationSurface, inFullImmersive); updateMaximizeMenu(startT, inFullImmersive); @@ -717,7 +717,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } private void notifyCaptionStateChanged() { - // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode. if (!canEnterDesktopMode(mContext) || !isEducationEnabled()) { return; } @@ -847,6 +846,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnCaptionButtonClickListener, mOnCaptionLongClickListener, mOnCaptionGenericMotionListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + mOnMaximizeOrRestoreClickListener, mOnMaximizeHoverListener); } throw new IllegalArgumentException("Unexpected layout resource id"); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 053850480ecc..32a2f8294877 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -44,6 +44,8 @@ import android.window.SurfaceSyncGroup import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.compose.ui.graphics.toArgb +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R @@ -55,8 +57,8 @@ import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer import com.android.wm.shell.windowdecor.common.DecorThemeUtil -import com.android.wm.shell.windowdecor.common.calculateMenuPosition import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader +import com.android.wm.shell.windowdecor.common.calculateMenuPosition import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.isPinned @@ -536,6 +538,20 @@ class HandleMenu( } return@setOnTouchListener true } + + with(context.resources) { + // Update a11y read out to say "double tap to enter desktop windowing mode" + ViewCompat.replaceAccessibilityAction( + desktopBtn, ACTION_CLICK, + getString(R.string.app_handle_menu_talkback_desktop_mode_button_text), null + ) + + // Update a11y read out to say "double tap to enter split screen mode" + ViewCompat.replaceAccessibilityAction( + splitscreenBtn, ACTION_CLICK, + getString(R.string.app_handle_menu_talkback_split_screen_mode_button_text), null + ) + } } /** Binds the menu views to the new data. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 1ce0366728b9..be3ea4e7b71e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -34,6 +34,7 @@ import android.graphics.drawable.LayerDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.StateListDrawable import android.graphics.drawable.shapes.RoundRectShape +import android.os.Bundle import android.util.StateSet import android.view.LayoutInflater import android.view.MotionEvent.ACTION_HOVER_ENTER @@ -51,12 +52,16 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.WindowlessWindowManager import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.Button import android.widget.TextView import android.window.TaskConstants import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.toArgb import androidx.core.animation.addListener +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import androidx.core.view.isGone import androidx.core.view.isVisible import com.android.wm.shell.R @@ -403,6 +408,96 @@ class MaximizeMenu( true } + sizeToggleButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onMaximizeClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + snapLeftButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onLeftSnapClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + snapRightButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onRightSnapClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + with(context.resources) { + ViewCompat.replaceAccessibilityAction( + snapLeftButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_snap_left_text), + null + ) + + ViewCompat.replaceAccessibilityAction( + snapRightButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_snap_right_text), + null + ) + + ViewCompat.replaceAccessibilityAction( + sizeToggleButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_maximize_restore_text), + null + ) + } + // Maximize/restore button. val sizeToggleBtnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE) R.string.desktop_mode_maximize_menu_restore_button_text diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 9f8ca7740182..db12f899f42f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -27,10 +27,13 @@ import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RoundRectShape +import android.os.Bundle import android.view.View import android.view.View.OnLongClickListener import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView @@ -49,6 +52,8 @@ import com.android.internal.R.color.materialColorSurfaceDim import com.android.window.flags.Flags import com.android.wm.shell.R import android.window.DesktopModeFlags +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import com.android.wm.shell.windowdecor.MaximizeButtonView import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.OPACITY_100 @@ -71,7 +76,10 @@ class AppHeaderViewHolder( onCaptionButtonClickListener: View.OnClickListener, private val onLongClickListener: OnLongClickListener, onCaptionGenericMotionListener: View.OnGenericMotionListener, - onMaximizeHoverAnimationFinishedListener: () -> Unit + mOnLeftSnapClickListener: () -> Unit, + mOnRightSnapClickListener: () -> Unit, + mOnMaximizeOrRestoreClickListener: () -> Unit, + onMaximizeHoverAnimationFinishedListener: () -> Unit, ) : WindowDecorationViewHolder<AppHeaderViewHolder.HeaderData>(rootView) { data class HeaderData( @@ -153,6 +161,91 @@ class AppHeaderViewHolder( minimizeWindowButton.setOnTouchListener(onCaptionTouchListener) maximizeButtonView.onHoverAnimationFinishedListener = onMaximizeHoverAnimationFinishedListener + + val a11yActionSnapLeft = AccessibilityAction( + R.id.action_snap_left, + context.resources.getString(R.string.desktop_mode_a11y_action_snap_left) + ) + val a11yActionSnapRight = AccessibilityAction( + R.id.action_snap_right, + context.resources.getString(R.string.desktop_mode_a11y_action_snap_right) + ) + val a11yActionMaximizeRestore = AccessibilityAction( + R.id.action_maximize_restore, + context.resources.getString(R.string.desktop_mode_a11y_action_maximize_restore) + ) + + captionHandle.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(a11yActionSnapLeft) + info.addAction(a11yActionSnapRight) + info.addAction(a11yActionMaximizeRestore) + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + when (action) { + R.id.action_snap_left -> mOnLeftSnapClickListener.invoke() + R.id.action_snap_right -> mOnRightSnapClickListener.invoke() + R.id.action_maximize_restore -> mOnMaximizeOrRestoreClickListener.invoke() + } + + return super.performAccessibilityAction(host, action, args) + } + } + maximizeWindowButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + info.addAction(a11yActionSnapLeft) + info.addAction(a11yActionSnapRight) + info.addAction(a11yActionMaximizeRestore) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + when (action) { + AccessibilityAction.ACTION_CLICK.id -> host.performClick() + R.id.action_snap_left -> mOnLeftSnapClickListener.invoke() + R.id.action_snap_right -> mOnRightSnapClickListener.invoke() + R.id.action_maximize_restore -> mOnMaximizeOrRestoreClickListener.invoke() + } + + return super.performAccessibilityAction(host, action, args) + } + } + + with(context.resources) { + // Update a11y read out to say "double tap to maximize or restore window size" + ViewCompat.replaceAccessibilityAction( + maximizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_button_talkback_action_maximize_restore_text), + null + ) + + // Update a11y read out to say "double tap to minimize app window" + ViewCompat.replaceAccessibilityAction( + minimizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.minimize_button_talkback_action_maximize_restore_text), + null + ) + } } override fun bindData(data: HeaderData) { @@ -628,6 +721,9 @@ class AppHeaderViewHolder( onCaptionButtonClickListener: View.OnClickListener, onLongClickListener: OnLongClickListener, onCaptionGenericMotionListener: View.OnGenericMotionListener, + mOnLeftSnapClickListener: () -> Unit, + mOnRightSnapClickListener: () -> Unit, + mOnMaximizeOrRestoreClickListener: () -> Unit, onMaximizeHoverAnimationFinishedListener: () -> Unit, ): AppHeaderViewHolder = AppHeaderViewHolder( rootView, @@ -635,6 +731,9 @@ class AppHeaderViewHolder( onCaptionButtonClickListener, onLongClickListener, onCaptionGenericMotionListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + mOnMaximizeOrRestoreClickListener, onMaximizeHoverAnimationFinishedListener, ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt index 7d2a8082c43e..1160a9286572 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt @@ -175,6 +175,64 @@ class AppHandleEducationControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_noCaptionStateNotified_shouldHideAllTooltips() = + testScope.runTest { + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, times(1)).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHandleHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_enterDesktopModeHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(enterDesktopModeHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_exitDesktopModeHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(exitDesktopModeHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun init_flagDisabled_shouldNotCallShowEducationTooltip() = testScope.runTest { @@ -289,8 +347,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { // Mark app handle hint viewed. testDataStoreFlow.value = createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) - val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education" - whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) + whenever(SystemProperties.getBoolean(eq(FORCE_SHOW_EDUCATION_SYSPROP), anyBoolean())) .thenReturn(true) setShouldShowDesktopModeEducation(true) @@ -396,5 +453,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { private companion object { val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long = APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L + + val FORCE_SHOW_EDUCATION_SYSPROP = "persist.windowing_force_show_desktop_mode_education" } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 87198d14c839..76e1e805f5a9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -297,7 +297,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt())) .thenReturn(false); - when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any())) + when(mMockAppHeaderViewHolderFactory + .create(any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(mMockAppHeaderViewHolder); when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mDesktopRepository); when(mMockDesktopUserRepositories.getProfile(anyInt())).thenReturn(mDesktopRepository); diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index 8bc66a048d27..f308ce953680 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -18,6 +18,7 @@ package android.media; import static android.media.audio.Flags.FLAG_DOLBY_AC4_LEVEL4_ENCODING_API; import static android.media.audio.Flags.FLAG_IAMF_DEFINITIONS_API; +import static android.media.audio.Flags.FLAG_SONY_360RA_MPEGH_3D_FORMAT; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -718,8 +719,9 @@ public final class AudioFormat implements Parcelable { * Same as 9.1.4 with the addition of left and right top side channels */ public static final int CHANNEL_OUT_9POINT1POINT6 = (CHANNEL_OUT_9POINT1POINT4 | CHANNEL_OUT_TOP_SIDE_LEFT | CHANNEL_OUT_TOP_SIDE_RIGHT); - /** @hide */ - public static final int CHANNEL_OUT_13POINT_360RA = ( + /** Output channel mask for 13.0 */ + @FlaggedApi(FLAG_SONY_360RA_MPEGH_3D_FORMAT) + public static final int CHANNEL_OUT_13POINT0 = ( CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_FRONT_RIGHT | CHANNEL_OUT_SIDE_LEFT | CHANNEL_OUT_SIDE_RIGHT | CHANNEL_OUT_TOP_FRONT_LEFT | CHANNEL_OUT_TOP_FRONT_CENTER | @@ -915,7 +917,7 @@ public final class AudioFormat implements Parcelable { case CHANNEL_OUT_9POINT1POINT6: result.append("9.1.6"); break; - case CHANNEL_OUT_13POINT_360RA: + case CHANNEL_OUT_13POINT0: result.append("360RA 13ch"); break; case CHANNEL_OUT_22POINT2: diff --git a/media/java/android/media/audio/common/AidlConversion.java b/media/java/android/media/audio/common/AidlConversion.java index 8521d1c472a8..b831e4f83e02 100644 --- a/media/java/android/media/audio/common/AidlConversion.java +++ b/media/java/android/media/audio/common/AidlConversion.java @@ -366,8 +366,8 @@ public class AidlConversion { return AudioFormat.CHANNEL_OUT_9POINT1POINT4; case AudioChannelLayout.LAYOUT_9POINT1POINT6: return AudioFormat.CHANNEL_OUT_9POINT1POINT6; - case AudioChannelLayout.LAYOUT_13POINT_360RA: - return AudioFormat.CHANNEL_OUT_13POINT_360RA; + case AudioChannelLayout.LAYOUT_13POINT0: + return AudioFormat.CHANNEL_OUT_13POINT0; case AudioChannelLayout.LAYOUT_22POINT2: return AudioFormat.CHANNEL_OUT_22POINT2; case AudioChannelLayout.LAYOUT_MONO_HAPTIC_A: diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt index 4c329dcf2f2b..cebd05d92537 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt @@ -18,20 +18,12 @@ package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata -import android.content.ContentResolver -import android.content.applicationContext import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.bluetooth.BluetoothEventManager import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant -import com.android.settingslib.bluetooth.LocalBluetoothProfileManager -import com.android.settingslib.bluetooth.VolumeControlProfile -import com.android.settingslib.volume.shared.AudioSharingLogger 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.testKosmos import com.google.common.truth.Truth.assertThat @@ -46,14 +38,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.whenever @@ -78,7 +66,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testIsAudioSharingOn_flagOff_false() = + fun isAudioSharingOn_flagOff_false() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false) @@ -90,7 +78,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testIsAudioSharingOn_flagOn_notInAudioSharing_false() = + fun isAudioSharingOn_flagOn_notInAudioSharing_false() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -103,7 +91,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testIsAudioSharingOn_flagOn_inAudioSharing_true() = + fun isAudioSharingOn_flagOn_inAudioSharing_true() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -116,7 +104,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testAudioSourceStateUpdate_notInAudioSharing_returnEmpty() = + fun audioSourceStateUpdate_notInAudioSharing_returnEmpty() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -129,7 +117,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testAudioSourceStateUpdate_inAudioSharing_returnUnit() = + fun audioSourceStateUpdate_inAudioSharing_returnUnit() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -144,7 +132,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testHandleAudioSourceWhenReady_flagOff_sourceNotAdded() = + fun handleAudioSourceWhenReady_flagOff_sourceNotAdded() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false) @@ -157,7 +145,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testHandleAudioSourceWhenReady_noProfile_sourceNotAdded() = + fun handleAudioSourceWhenReady_noProfile_sourceNotAdded() = with(kosmos) { testScope.runTest { bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) @@ -171,36 +159,41 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() = + fun handleAudioSourceWhenReady_hasProfileButAudioSharingNeverTriggered_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) - runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped + verify(localBluetoothLeBroadcast).registerServiceCallBack(any(), any()) assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() job.cancel() } } @Test - fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() = + fun handleAudioSourceWhenReady_audioSharingTriggeredButFailed_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + // Verify callback registered for onBroadcastStartedOrStopped + verify(localBluetoothLeBroadcast) + .registerServiceCallBack(any(), callbackCaptor.capture()) + // Audio sharing started failed, trigger onBroadcastStartFailed + whenever(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(false) + underTest.startAudioSharing() + runCurrent() + callbackCaptor.value.onBroadcastStartFailed(0) runCurrent() assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() @@ -209,122 +202,59 @@ class AudioSharingInteractorTest : SysuiTestCase() { } @Test - fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() = + fun handleAudioSourceWhenReady_audioSharingTriggeredButMetadataNotReady_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) - runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped verify(localBluetoothLeBroadcast) .registerServiceCallBack(any(), callbackCaptor.capture()) runCurrent() - callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata) + underTest.startAudioSharing() runCurrent() + // Verify callback registered for onBroadcastMetadataChanged + verify(localBluetoothLeBroadcast, times(2)) + .registerServiceCallBack(any(), callbackCaptor.capture()) - assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue() - job.cancel() - } - } - - @Test - fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() = - with(kosmos) { - testScope.runTest { - val (broadcast, repository) = setupRepositoryImpl() - val interactor = - object : - AudioSharingInteractorImpl( - applicationContext, - localBluetoothManager, - repository, - testDispatcher, - ) { - override suspend fun audioSharingAvailable() = true - } - val job = launch { interactor.handleAudioSourceWhenReady() } - runCurrent() - // Verify callback registered for onBroadcastStartedOrStopped - verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) - runCurrent() - // Verify source is not added - verify(repository, never()).addSource() + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() job.cancel() } } @Test - fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() = + fun handleAudioSourceWhenReady_audioSharingTriggeredAndMetadataReady_sourceAdded() = with(kosmos) { testScope.runTest { - val (broadcast, repository) = setupRepositoryImpl() - val interactor = - object : - AudioSharingInteractorImpl( - applicationContext, - localBluetoothManager, - repository, - testDispatcher, - ) { - override suspend fun audioSharingAvailable() = true - } - val job = launch { interactor.handleAudioSourceWhenReady() } + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( + localBluetoothLeBroadcast + ) + val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() // Verify callback registered for onBroadcastStartedOrStopped - verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) + verify(localBluetoothLeBroadcast) + .registerServiceCallBack(any(), callbackCaptor.capture()) // Audio sharing started, trigger onBroadcastStarted - whenever(broadcast.isEnabled(null)).thenReturn(true) + whenever(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(true) + underTest.startAudioSharing() + runCurrent() callbackCaptor.value.onBroadcastStarted(0, 0) runCurrent() // Verify callback registered for onBroadcastMetadataChanged - verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture()) + verify(localBluetoothLeBroadcast, times(2)) + .registerServiceCallBack(any(), callbackCaptor.capture()) runCurrent() // Trigger onBroadcastMetadataChanged (ready to add source) callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata) runCurrent() - // Verify source added - verify(repository).addSource() + + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue() job.cancel() } } - - private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> { - with(kosmos) { - val broadcast = - mock<LocalBluetoothLeBroadcast> { - on { isProfileReady } doReturn true - on { isEnabled(null) } doReturn false - } - val assistant = - mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true } - val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true } - val profileManager = - mock<LocalBluetoothProfileManager> { - on { leAudioBroadcastProfile } doReturn broadcast - on { leAudioBroadcastAssistantProfile } doReturn assistant - on { volumeControlProfile } doReturn volumeControl - } - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {}) - - val repository = - AudioSharingRepositoryImpl( - localBluetoothManager, - com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl( - mock<ContentResolver> {}, - localBluetoothManager, - testScope.backgroundScope, - testScope.testScheduler, - mock<AudioSharingLogger> {}, - ), - testDispatcher, - ) - return Pair(broadcast, spy(repository)) - } - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 555c717e1e65..93d1f593e81f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -78,6 +78,7 @@ import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -220,6 +221,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { mock(), { configurationForwarder }, brightnessMirrorShowingInteractor, + UnconfinedTestDispatcher(), ) controller.setupExpandedStatusBar() diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt index d82311f6ca7c..832afb1799b1 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -22,20 +22,25 @@ import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.onBroadcastMetadataChanged +import com.android.settingslib.bluetooth.onBroadcastStartedOrStopped import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.withContext /** Holds business logic for the audio sharing state. */ @@ -71,6 +76,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, ) : AudioSharingInteractor { + private val audioSharingStartedEvents = Channel<Unit>(Channel.BUFFERED) private var previewEnabled: Boolean? = null override val isAudioSharingOn: Flow<Boolean> = @@ -99,12 +105,18 @@ constructor( withContext(backgroundDispatcher) { if (audioSharingAvailable()) { audioSharingRepository.leAudioBroadcastProfile?.let { profile -> - isAudioSharingOn - // Skip the default value, we only care about adding source for newly - // started audio sharing session - .drop(1) - .mapNotNull { audioSharingOn -> - if (audioSharingOn) { + merge( + // Register and start listen to onBroadcastMetadataChanged (means ready + // to add source) + audioSharingStartedEvents.receiveAsFlow().map { true }, + // When session is off or failed to start, stop listening to + // onBroadcastMetadataChanged as we won't be adding source + profile.onBroadcastStartedOrStopped + .filterNot { profile.isEnabled(null) } + .map { false }, + ) + .mapNotNull { shouldListenToMetadata -> + if (shouldListenToMetadata) { // onBroadcastMetadataChanged could emit multiple times during one // audio sharing session, we only perform add source on the first // time @@ -146,6 +158,7 @@ constructor( if (!audioSharingAvailable()) { return } + audioSharingStartedEvents.trySend(Unit) audioSharingRepository.startAudioSharing() } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 0c8196693bc4..9b24c69cac30 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -16,8 +16,6 @@ package com.android.systemui.media.dialog; -import static com.android.systemui.media.dialog.MediaOutputSeekbar.VOLUME_PERCENTAGE_SCALE_SIZE; - import android.animation.Animator; import android.animation.ValueAnimator; import android.app.WallpaperColors; @@ -289,10 +287,7 @@ public abstract class MediaOutputBaseAdapter extends } } else { if (!mVolumeAnimator.isStarted()) { - int percentage = - (int) ((double) currentVolume * VOLUME_PERCENTAGE_SCALE_SIZE - / (double) mSeekBar.getMax()); - if (percentage == 0) { + if (currentVolume == 0) { updateMutedVolumeIcon(device); } else { updateUnmutedVolumeIcon(device); @@ -319,20 +314,20 @@ public abstract class MediaOutputBaseAdapter extends if (device == null || !fromUser) { return; } - int progressToVolume = MediaOutputSeekbar.scaleProgressToVolume(progress); - int deviceVolume = device.getCurrentVolume(); - int percentage = - (int) ((double) progressToVolume * VOLUME_PERCENTAGE_SCALE_SIZE - / (double) seekBar.getMax()); - mVolumeValueText.setText(mContext.getResources().getString( - R.string.media_output_dialog_volume_percentage, percentage)); + + final String percentageString = mContext.getResources().getString( + R.string.media_output_dialog_volume_percentage, + mSeekBar.getPercentage()); + mVolumeValueText.setText(percentageString); + if (mStartFromMute) { updateUnmutedVolumeIcon(device); mStartFromMute = false; } - if (progressToVolume != deviceVolume) { - mLatestUpdateVolume = progressToVolume; - mController.adjustVolume(device, progressToVolume); + int seekBarVolume = MediaOutputSeekbar.scaleProgressToVolume(progress); + if (seekBarVolume != device.getCurrentVolume()) { + mLatestUpdateVolume = seekBarVolume; + mController.adjustVolume(device, seekBarVolume); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java index be5d60799f79..b7381dafcf12 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java @@ -16,22 +16,62 @@ package com.android.systemui.media.dialog; +import android.annotation.Nullable; import android.content.Context; import android.util.AttributeSet; import android.widget.SeekBar; +import com.android.systemui.res.R; + /** * Customized SeekBar for MediaOutputDialog, apply scale between device volume and progress, to make * adjustment smoother. */ public class MediaOutputSeekbar extends SeekBar { + // The scale is added to make slider value change smooth. private static final int SCALE_SIZE = 1000; - private static final int INITIAL_PROGRESS = 500; - public static final int VOLUME_PERCENTAGE_SCALE_SIZE = 100000; + + @Nullable + private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = null; public MediaOutputSeekbar(Context context, AttributeSet attrs) { super(context, attrs); setMin(0); + super.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final String percentageString = context.getResources().getString( + R.string.media_output_dialog_volume_percentage, + getPercentage()); + // Override the default TTS for the seekbar. The percentage should correspond to + // the volume value, not the progress value. I.e. for the volume range 0 - 25, the + // percentage should be 0%, 4%, 8%, etc. It should never be 6% since 6% doesn't map + // to an integer volume value. + setStateDescription(percentageString); + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStartTrackingTouch(seekBar); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStopTrackingTouch(seekBar); + } + } + }); + } + + @Override + public void setOnSeekBarChangeListener(@Nullable SeekBar.OnSeekBarChangeListener listener) { + mOnSeekBarChangeListener = listener; } static int scaleProgressToVolume(int progress) { @@ -39,11 +79,11 @@ public class MediaOutputSeekbar extends SeekBar { } static int scaleVolumeToProgress(int volume) { - return volume == 0 ? 0 : INITIAL_PROGRESS + volume * SCALE_SIZE; + return volume * SCALE_SIZE; } int getVolume() { - return getProgress() / SCALE_SIZE; + return scaleProgressToVolume(getProgress()); } void setVolume(int volume) { @@ -51,10 +91,18 @@ public class MediaOutputSeekbar extends SeekBar { } void setMaxVolume(int maxVolume) { - setMax(maxVolume * SCALE_SIZE); + setMax(scaleVolumeToProgress(maxVolume)); } void resetVolume() { setProgress(getMin()); } + + int getPercentage() { + // The progress -> volume -> progress conversion is necessary to ensure that progress + // strictly corresponds to an integer volume value. + // Example: 10424 (progress) -> 10 (volume) -> 10000 (progress). + int normalizedProgress = scaleVolumeToProgress(scaleProgressToVolume(getProgress())); + return (int) ((double) normalizedProgress * 100 / getMax()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 255494f014e3..10a9fd20ee5a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -45,6 +45,7 @@ import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags; import com.android.systemui.bouncer.ui.binder.BouncerViewBinder; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; @@ -87,6 +88,8 @@ import com.android.systemui.util.time.SystemClock; import com.android.systemui.window.ui.WindowRootViewBinder; import com.android.systemui.window.ui.viewmodel.WindowRootViewModel; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.ExperimentalCoroutinesApi; import kotlinx.coroutines.flow.Flow; import java.io.PrintWriter; @@ -119,6 +122,7 @@ public class NotificationShadeWindowViewController implements Dumpable { private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final QuickSettingsController mQuickSettingsController; + private final CoroutineDispatcher mMainDispatcher; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; private final GlanceableHubContainerController mGlanceableHubContainerController; @@ -204,7 +208,8 @@ public class NotificationShadeWindowViewController implements Dumpable { AlternateBouncerInteractor alternateBouncerInteractor, BouncerViewBinder bouncerViewBinder, @ShadeDisplayAware Provider<ConfigurationForwarder> configurationForwarder, - BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor) { + BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor, + @Main CoroutineDispatcher mainDispatcher) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; mStatusBarStateController = statusBarStateController; @@ -232,6 +237,7 @@ public class NotificationShadeWindowViewController implements Dumpable { mPrimaryBouncerInteractor = primaryBouncerInteractor; mAlternateBouncerInteractor = alternateBouncerInteractor; mQuickSettingsController = quickSettingsController; + mMainDispatcher = mainDispatcher; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -286,7 +292,7 @@ public class NotificationShadeWindowViewController implements Dumpable { if (SceneContainerFlag.isEnabled()) return; WindowRootViewBinder.INSTANCE.bind(mView, windowRootViewModelFactory, blurUtils, - choreographer); + choreographer, mMainDispatcher); } private void bindBouncer(BouncerViewBinder bouncerViewBinder) { diff --git a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt index 7f4bfb094a7b..e09a74cd0ad3 100644 --- a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt @@ -26,6 +26,7 @@ import com.android.systemui.lifecycle.viewModel import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.statusbar.BlurUtils import com.android.systemui.window.ui.viewmodel.WindowRootViewModel +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -42,11 +43,12 @@ object WindowRootViewBinder { viewModelFactory: WindowRootViewModel.Factory, blurUtils: BlurUtils?, choreographer: Choreographer?, + mainDispatcher: CoroutineDispatcher, ) { if (!Flags.bouncerUiRevamp() && !Flags.glanceableHubBlurredBackground()) return if (blurUtils == null || choreographer == null) return - view.repeatWhenAttached { + view.repeatWhenAttached(mainDispatcher) { Log.d(TAG, "Binding root view") var frameCallbackPendingExecution: FrameCallback? = null view.viewModel( diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index b5eb90402f43..676d8fa06d82 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -235,6 +235,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa modifyNotification(context).also { it.setSmallIcon(android.R.drawable.ic_media_pause) it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setContentIntent(getNewPendingIntent()) } build() } @@ -2156,6 +2157,28 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) } + @Test + @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) + fun postDifferentIntentNotifications_CallsListeners() { + addNotificationAndLoad() + reset(listener) + mediaNotification = + mediaNotification.also { it.notification.contentIntent = getNewPendingIntent() } + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + testScope.assertRunAllReady(foreground = 1, background = 1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false), + ) + verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) + } + private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) { runCurrent() if (Flags.mediaLoadMetadataViaMediaDataLoader()) { @@ -2235,4 +2258,14 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() } + + private fun getNewPendingIntent(): PendingIntent { + val intent = Intent().setAction(null) + return PendingIntent.getBroadcast( + mContext, + 1, + intent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } } 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 042d30ee23a2..496b31990b9d 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 @@ -251,6 +251,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor) verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor) session = MediaSession(context, "MediaDataProcessorTestSession") + mediaNotification = SbnBuilder().run { setUser(UserHandle(USER_ID)) @@ -258,6 +259,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { modifyNotification(context).also { it.setSmallIcon(android.R.drawable.ic_media_pause) it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setContentIntent(getNewPendingIntent()) } build() } @@ -2250,6 +2252,33 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) } + @Test + @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) + fun postDifferentIntentNotifications_CallsListeners() { + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + + mediaDataProcessor.addInternalListener(mediaDataFilter) + mediaDataFilter.mediaDataProcessor = mediaDataProcessor + addNotificationAndLoad() + reset(listener) + mediaNotification = + mediaNotification.also { it.notification.contentIntent = getNewPendingIntent() } + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + testScope.assertRunAllReady(foreground = 1, background = 1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false), + ) + verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) + } + private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) { runCurrent() if (Flags.mediaLoadMetadataViaMediaDataLoader()) { @@ -2329,4 +2358,14 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { runCurrent() advanceUntilIdle() } + + private fun getNewPendingIntent(): PendingIntent { + val intent = Intent().setAction(null) + return PendingIntent.getBroadcast( + mContext, + 1, + intent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 70450d29c74e..49d6909c1f93 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -254,6 +254,7 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : mock(BouncerViewBinder::class.java), { mock(ConfigurationForwarder::class.java) }, brightnessMirrorShowingInteractor, + kosmos.testDispatcher, ) underTest.setupExpandedStatusBar() underTest.setDragDownHelper(dragDownHelper) diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index 2d802b21cf03..b6768c9c087a 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -38,7 +38,6 @@ import android.hardware.biometrics.AuthenticationStateListener; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.ComponentInfoInternal; -import android.hardware.biometrics.Flags; import android.hardware.biometrics.IAuthService; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; @@ -399,12 +398,6 @@ public class AuthService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { - // We can't do this above because we need the READ_DEVICE_CONFIG permission, which - // the calling user may not possess. - if (!Flags.lastAuthenticationTime()) { - throw new UnsupportedOperationException(); - } - return mBiometricService.getLastAuthenticationTime(userId, authenticators); } finally { Binder.restoreCallingIdentity(identity); diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 15b1f220bc3c..a5058dd51a33 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -44,7 +44,6 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricStateListener; -import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricSensorReceiver; @@ -906,10 +905,6 @@ public class BiometricService extends SystemService { int userId, @Authenticators.Types int authenticators) { super.getLastAuthenticationTime_enforcePermission(); - if (!Flags.lastAuthenticationTime()) { - throw new UnsupportedOperationException(); - } - Slogf.d(TAG, "getLastAuthenticationTime(userId=%d, authenticators=0x%x)", userId, authenticators); diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index b8b49f3eed2f..f9758fcd5d01 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -26,13 +26,13 @@ import android.content.om.OverlayInfo; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IndentingPrintWriter; import android.util.Pair; import android.util.Slog; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.CollectionUtils; -import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -49,7 +49,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Stream; /** * Data structure representing the current state of all overlay packages in the @@ -358,26 +357,29 @@ final class OverlayManagerSettings { } void dump(@NonNull final PrintWriter p, @NonNull DumpState dumpState) { - // select items to display - Stream<SettingsItem> items = mItems.stream(); - if (dumpState.getUserId() != UserHandle.USER_ALL) { - items = items.filter(item -> item.mUserId == dumpState.getUserId()); - } - if (dumpState.getPackageName() != null) { - items = items.filter(item -> item.mOverlay.getPackageName() - .equals(dumpState.getPackageName())); - } - if (dumpState.getOverlayName() != null) { - items = items.filter(item -> item.mOverlay.getOverlayName() - .equals(dumpState.getOverlayName())); - } - - // display items - final IndentingPrintWriter pw = new IndentingPrintWriter(p, " "); - if (dumpState.getField() != null) { - items.forEach(item -> dumpSettingsItemField(pw, item, dumpState.getField())); - } else { - items.forEach(item -> dumpSettingsItem(pw, item)); + final int userId = dumpState.getUserId(); + final String packageName = dumpState.getPackageName(); + final String overlayName = dumpState.getOverlayName(); + final String field = dumpState.getField(); + final var pw = new IndentingPrintWriter(p, " "); + + for (int i = 0; i < mItems.size(); i++) { + final var item = mItems.get(i); + if (userId != UserHandle.USER_ALL && userId != item.mUserId) { + continue; + } + if (packageName != null && !packageName.equals(item.mOverlay.getPackageName())) { + continue; + } + if (overlayName != null && !overlayName.equals(item.mOverlay.getOverlayName())) { + continue; + } + + if (field != null) { + dumpSettingsItemField(pw, item, field); + } else { + dumpSettingsItem(pw, item); + } } } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 81c7807311dd..c7d4467a6e98 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -71,6 +71,8 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_USER_LEAVING; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CONFIGURATION; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_FOCUS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_USER_LEAVING; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; @@ -89,7 +91,9 @@ import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK; import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -153,6 +157,8 @@ import com.android.server.wm.TaskFragment.EmbeddingCheckResult; import com.android.wm.shell.Flags; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.text.DateFormat; import java.util.Date; import java.util.function.Supplier; @@ -166,6 +172,8 @@ import java.util.function.Supplier; class ActivityStarter { private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityStarter" : TAG_ATM; private static final String TAG_RESULTS = TAG + POSTFIX_RESULTS; + private static final String TAG_FOCUS = TAG + POSTFIX_FOCUS; + private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION; private static final String TAG_USER_LEAVING = TAG + POSTFIX_USER_LEAVING; private static final int INVALID_LAUNCH_MODE = -1; @@ -247,7 +255,26 @@ class ActivityStarter { private boolean mIsTaskCleared; private boolean mMovedToFront; private boolean mNoAnimation; - private boolean mAvoidMoveToFront; + + // TODO mAvoidMoveToFront before V is changed from a boolean to a int code mCanMoveToFrontCode + // for the purpose of attribution of new BAL V feature. This should be reverted back to the + // boolean flag post V. + @IntDef(prefix = {"MOVE_TO_FRONT_"}, value = { + MOVE_TO_FRONT_ALLOWED, + MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS, + MOVE_TO_FRONT_AVOID_LEGACY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface MoveToFrontCode {} + + // Allows a task move to front. + private static final int MOVE_TO_FRONT_ALLOWED = 0; + // Avoid a task move to front because the Pending Intent that starts the activity only + // its creator has the BAL privilege, its sender does not. + private static final int MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS = 1; + // Avoid a task move to front because of all other legacy reasons. + private static final int MOVE_TO_FRONT_AVOID_LEGACY = 2; + private @MoveToFrontCode int mCanMoveToFrontCode = MOVE_TO_FRONT_ALLOWED; private boolean mFrozeTaskList; private boolean mTransientLaunch; // The task which was above the targetTask before starting this activity. null if the targetTask @@ -744,7 +771,7 @@ class ActivityStarter { mIsTaskCleared = starter.mIsTaskCleared; mMovedToFront = starter.mMovedToFront; mNoAnimation = starter.mNoAnimation; - mAvoidMoveToFront = starter.mAvoidMoveToFront; + mCanMoveToFrontCode = starter.mCanMoveToFrontCode; mFrozeTaskList = starter.mFrozeTaskList; mVoiceSession = starter.mVoiceSession; @@ -1684,6 +1711,14 @@ class ActivityStarter { return result; } + private boolean avoidMoveToFront() { + return mCanMoveToFrontCode != MOVE_TO_FRONT_ALLOWED; + } + + private boolean avoidMoveToFrontPIOnlyCreatorAllows() { + return mCanMoveToFrontCode == MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS; + } + /** * If the start result is success, ensure that the configuration of the started activity matches * the current display. Otherwise clean up unassociated containers to avoid leakage. @@ -1733,7 +1768,7 @@ class ActivityStarter { startedActivityRootTask.setAlwaysOnTop(true); } - if (isIndependentLaunch && !mDoResume && mAvoidMoveToFront && !mTransientLaunch + if (isIndependentLaunch && !mDoResume && avoidMoveToFront() && !mTransientLaunch && !started.shouldBeVisible(true /* ignoringKeyguard */)) { Slog.i(TAG, "Abort " + transition + " of invisible launch " + started); transition.abort(); @@ -1749,7 +1784,7 @@ class ActivityStarter { currentTop, currentTop.mDisplayContent, false /* deferResume */); } - if (!mAvoidMoveToFront && mDoResume + if (!avoidMoveToFront() && mDoResume && !mService.getUserManagerInternal().isVisibleBackgroundFullUser(started.mUserId) && mRootWindowContainer.hasVisibleWindowAboveButDoesNotOwnNotificationShade( started.launchedFromUid)) { @@ -1899,17 +1934,19 @@ class ActivityStarter { } // When running transient transition, the transient launch target should keep on top. // So disallow the transient hide activity to move itself to front, e.g. trampoline. - if (!mAvoidMoveToFront && (mService.mHomeProcess == null + if (!avoidMoveToFront() && (mService.mHomeProcess == null || mService.mHomeProcess.mUid != realCallingUid) && (prevTopTask != null && prevTopTask.isActivityTypeHomeOrRecents()) && r.mTransitionController.isTransientHide(targetTask)) { - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; } // If the activity is started by sending a pending intent and only its creator has the // privilege to allow BAL (its sender does not), avoid move it to the front. Only do // this when it is not a new task and not already been marked as avoid move to front. - if (!mAvoidMoveToFront && balVerdict.onlyCreatorAllows()) { - mAvoidMoveToFront = true; + // Guarded by a flag: balDontBringExistingBackgroundTaskStackToFg + if (balDontBringExistingBackgroundTaskStackToFg() && !avoidMoveToFront() + && balVerdict.onlyCreatorAllows()) { + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS; } mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask()); } @@ -1966,28 +2003,32 @@ class ActivityStarter { // After activity is attached to task, but before actual start recordTransientLaunchIfNeeded(mLastStartActivityRecord); - if (!mAvoidMoveToFront && mDoResume) { - mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask); + if (mDoResume) { + if (!avoidMoveToFront()) { + mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask); + + final boolean launchBehindDream; + if (com.android.window.flags.Flags.removeActivityStarterDreamCallback()) { + final TaskDisplayArea tda = mTargetRootTask.getTaskDisplayArea(); + final Task top = (tda != null ? tda.getTopRootTask() : null); + launchBehindDream = (top != null && top != mTargetRootTask) + && top.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_DREAM + && top.getTopNonFinishingActivity() != null; + } else { + launchBehindDream = !mTargetRootTask.isTopRootTaskInDisplayArea() + && mService.isDreaming() + && !dreamStopping; + } - final boolean launchBehindDream; - if (com.android.window.flags.Flags.removeActivityStarterDreamCallback()) { - final TaskDisplayArea tda = mTargetRootTask.getTaskDisplayArea(); - final Task top = (tda != null ? tda.getTopRootTask() : null); - launchBehindDream = (top != null && top != mTargetRootTask) - && top.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_DREAM - && top.getTopNonFinishingActivity() != null; + if (launchBehindDream) { + // Launching underneath dream activity (fullscreen, always-on-top). Run the + // launch--behind transition so the Activity gets created and starts + // in visible state. + mLaunchTaskBehind = true; + r.mLaunchTaskBehind = true; + } } else { - launchBehindDream = !mTargetRootTask.isTopRootTaskInDisplayArea() - && mService.isDreaming() - && !dreamStopping; - } - - if (launchBehindDream) { - // Launching underneath dream activity (fullscreen, always-on-top). Run the - // launch--behind transition so the Activity gets created and starts - // in visible state. - mLaunchTaskBehind = true; - r.mLaunchTaskBehind = true; + logPIOnlyCreatorAllowsBAL(); } } @@ -2048,9 +2089,13 @@ class ActivityStarter { // root-task to the will not update the focused root-task. If starting the new // activity now allows the task root-task to be focusable, then ensure that we // now update the focused root-task accordingly. - if (!mAvoidMoveToFront && mTargetRootTask.isTopActivityFocusable() + if (mTargetRootTask.isTopActivityFocusable() && !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) { - mTargetRootTask.moveToFront("startActivityInner"); + if (!avoidMoveToFront()) { + mTargetRootTask.moveToFront("startActivityInner"); + } else { + logPIOnlyCreatorAllowsBAL(); + } } mRootWindowContainer.resumeFocusedTasksTopActivities( mTargetRootTask, mStartActivity, mOptions, mTransientLaunch); @@ -2078,6 +2123,26 @@ class ActivityStarter { return START_SUCCESS; } + // TODO (b/316135632) Post V release, remove this log method. + private void logPIOnlyCreatorAllowsBAL() { + if (!avoidMoveToFrontPIOnlyCreatorAllows()) return; + String realCallingPackage = + mService.mContext.getPackageManager().getNameForUid(mRealCallingUid); + if (realCallingPackage == null) { + realCallingPackage = "uid=" + mRealCallingUid; + } + Slog.wtf(TAG, "Without Android 15 BAL hardening this activity would be moved to the " + + "foreground. The activity is started by a PendingIntent. However, only the " + + "creator of the PendingIntent allows BAL while the sender does not allow BAL. " + + "realCallingPackage: " + realCallingPackage + + "; callingPackage: " + mRequest.callingPackage + + "; mTargetRootTask:" + mTargetRootTask + + "; mIntent: " + mIntent + + "; mTargetRootTask.getTopNonFinishingActivity: " + + mTargetRootTask.getTopNonFinishingActivity() + + "; mTargetRootTask.getRootActivity: " + mTargetRootTask.getRootActivity()); + } + private void recordTransientLaunchIfNeeded(ActivityRecord r) { if (r == null || !mTransientLaunch) return; final TransitionController controller = r.mTransitionController; @@ -2222,7 +2287,7 @@ class ActivityStarter { } if (!mSupervisor.getBackgroundActivityLaunchController().checkActivityAllowedToStart( - mSourceRecord, r, newTask, mAvoidMoveToFront, targetTask, mLaunchFlags, mBalCode, + mSourceRecord, r, newTask, avoidMoveToFront(), targetTask, mLaunchFlags, mBalCode, mCallingUid, mRealCallingUid, mPreferredTaskDisplayArea)) { return START_ABORTED; } @@ -2570,7 +2635,7 @@ class ActivityStarter { mIsTaskCleared = false; mMovedToFront = false; mNoAnimation = false; - mAvoidMoveToFront = false; + mCanMoveToFrontCode = MOVE_TO_FRONT_ALLOWED; mFrozeTaskList = false; mTransientLaunch = false; mPriorAboveTask = null; @@ -2682,12 +2747,12 @@ class ActivityStarter { // The caller specifies that we'd like to be avoided to be moved to the // front, so be it! mDoResume = false; - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; } } } else if (mOptions.getAvoidMoveToFront()) { mDoResume = false; - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; } mTransientLaunch = mOptions.getTransientLaunch(); final KeyguardController kc = mSupervisor.getKeyguardController(); @@ -2697,7 +2762,7 @@ class ActivityStarter { if (mTransientLaunch && mDisplayLockAndOccluded && mService.getTransitionController().isShellTransitionsEnabled()) { mDoResume = false; - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; } mTargetRootTask = Task.fromWindowContainerToken(mOptions.getLaunchRootTask()); @@ -2754,7 +2819,7 @@ class ActivityStarter { mNoAnimation = (mLaunchFlags & FLAG_ACTIVITY_NO_ANIMATION) != 0; if (mBalCode == BAL_BLOCK && !mService.isBackgroundActivityStartsEnabled()) { - mAvoidMoveToFront = true; + mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; mDoResume = false; } } @@ -2985,7 +3050,7 @@ class ActivityStarter { differentTopTask = true; } - if (differentTopTask && !mAvoidMoveToFront) { + if (differentTopTask && !avoidMoveToFront()) { mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); // We really do want to push this one into the user's face, right now. if (mLaunchTaskBehind && mSourceRecord != null) { @@ -3029,6 +3094,9 @@ class ActivityStarter { } mOptions = null; } + if (differentTopTask) { + logPIOnlyCreatorAllowsBAL(); + } // Update the target's launch cookie and pending remote animation to those specified in the // options if set. if (mStartActivity.mLaunchCookie != null) { @@ -3069,7 +3137,7 @@ class ActivityStarter { } private void setNewTask(Task taskToAffiliate) { - final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront; + final boolean toTop = !mLaunchTaskBehind && !avoidMoveToFront(); final Task task = mTargetRootTask.reuseOrCreateTask( mStartActivity.info, mIntent, mVoiceSession, mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 6df01f4b328b..119709e86551 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -45,11 +45,12 @@ import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONL import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; import static com.android.window.flags.Flags.balAdditionalStartModes; +import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balShowToastsBlocked; -import static com.android.window.flags.Flags.balStrictModeGracePeriod; import static com.android.window.flags.Flags.balStrictModeRo; +import static com.android.window.flags.Flags.balStrictModeGracePeriod; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Objects.requireNonNull; @@ -619,6 +620,8 @@ public class BackgroundActivityStartController { // features sb.append("; balRequireOptInByPendingIntentCreator: ") .append(balRequireOptInByPendingIntentCreator()); + sb.append("; balDontBringExistingBackgroundTaskStackToFg: ") + .append(balDontBringExistingBackgroundTaskStackToFg()); sb.append("]"); return sb.toString(); } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java index 605fed09dd9f..c7efa318af99 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java @@ -45,7 +45,6 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.hardware.biometrics.AuthenticationStateListener; import android.hardware.biometrics.BiometricManager; -import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceReceiver; @@ -504,23 +503,9 @@ public class AuthServiceTest { eq(callback)); } - @Test(expected = UnsupportedOperationException.class) - public void testGetLastAuthenticationTime_flaggedOff_throwsUnsupportedOperationException() - throws Exception { - mSetFlagsRule.disableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); - setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); - - mAuthService = new AuthService(mContext, mInjector); - mAuthService.onStart(); - - mAuthService.mImpl.getLastAuthenticationTime(0, - BiometricManager.Authenticators.BIOMETRIC_STRONG); - } - @Test - public void testGetLastAuthenticationTime_flaggedOn_callsBiometricService() + public void testGetLastAuthenticationTime_callsBiometricService() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); mAuthService = new AuthService(mContext, mInjector); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index acca4cc294b3..9918a9a35c33 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -2014,20 +2014,9 @@ public class BiometricServiceTest { verifyNoMoreInteractions(callback); } - @Test(expected = UnsupportedOperationException.class) - public void testGetLastAuthenticationTime_flagOff_throwsUnsupportedOperationException() - throws RemoteException { - mSetFlagsRule.disableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); - - mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider); - mBiometricService.mImpl.getLastAuthenticationTime(0, Authenticators.BIOMETRIC_STRONG); - } - @Test - public void testGetLastAuthenticationTime_flagOn_callsKeystoreAuthorization() + public void testGetLastAuthenticationTime_callsKeystoreAuthorization() throws RemoteException { - mSetFlagsRule.enableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); - final int[] hardwareAuthenticators = new int[] { HardwareAuthenticatorType.PASSWORD, HardwareAuthenticatorType.FINGERPRINT diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java index 902a58379ae0..51706d72cb35 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java @@ -589,7 +589,8 @@ public class BackgroundActivityStartControllerTests { + "realCallerApp: null; " + "balAllowedByPiSender: BSP.ALLOW_BAL; " + "realCallerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; " - + "balRequireOptInByPendingIntentCreator: true]"); + + "balRequireOptInByPendingIntentCreator: true; " + + "balDontBringExistingBackgroundTaskStackToFg: true]"); } @Test @@ -691,6 +692,7 @@ public class BackgroundActivityStartControllerTests { + "realCallerApp: null; " + "balAllowedByPiSender: BSP.ALLOW_FGS; " + "realCallerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; " - + "balRequireOptInByPendingIntentCreator: true]"); + + "balRequireOptInByPendingIntentCreator: true; " + + "balDontBringExistingBackgroundTaskStackToFg: true]"); } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 6d32303fb13b..73ea68bc3547 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -19380,7 +19380,6 @@ public class TelephonyManager { * * @hide */ - @FlaggedApi(Flags.FLAG_ENABLE_IDENTIFIER_DISCLOSURE_TRANSPARENCY) @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @SystemApi public void setEnableCellularIdentifierDisclosureNotifications(boolean enable) { @@ -19406,7 +19405,6 @@ public class TelephonyManager { * * @hide */ - @FlaggedApi(Flags.FLAG_ENABLE_IDENTIFIER_DISCLOSURE_TRANSPARENCY) @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @SystemApi public boolean isCellularIdentifierDisclosureNotificationsEnabled() { diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 7d07d42b8042..3ee6dc48bfa3 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -16,13 +16,11 @@ package android.testing; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.Instrumentation; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; -import android.os.SystemClock; import android.os.TestLooperManager; import android.util.ArrayMap; @@ -34,7 +32,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.LinkedList; +import java.lang.reflect.Field; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -58,6 +56,9 @@ public class TestableLooper { * catch crashes. */ public static final boolean HOLD_MAIN_THREAD = false; + private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; + private static final Field MESSAGE_NEXT_FIELD; + private static final Field MESSAGE_WHEN_FIELD; private Looper mLooper; private MessageQueue mQueue; @@ -66,6 +67,19 @@ public class TestableLooper { private Handler mHandler; private TestLooperManager mQueueWrapper; + static { + try { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Failed to initialize TestableLooper", e); + } + } + public TestableLooper(Looper l) throws Exception { this(acquireLooperManager(l), l); } @@ -208,17 +222,29 @@ public class TestableLooper { } public void moveTimeForward(long milliSeconds) { - long futureWhen = SystemClock.uptimeMillis() + milliSeconds; - // Find messages in the queue enqueued within the future time, and execute them now. - while (true) { - Long peekWhen = mQueueWrapper.peekWhen(); - if (peekWhen == null || peekWhen > futureWhen) { - break; - } - Message message = mQueueWrapper.poll(); - if (message != null) { - mQueueWrapper.execute(message); + try { + Message msg = getMessageLinkedList(); + while (msg != null) { + long updatedWhen = msg.getWhen() - milliSeconds; + if (updatedWhen < 0) { + updatedWhen = 0; + } + MESSAGE_WHEN_FIELD.set(msg, updatedWhen); + msg = (Message) MESSAGE_NEXT_FIELD.get(msg); } + } catch (IllegalAccessException e) { + throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e); + } + } + + private Message getMessageLinkedList() { + try { + MessageQueue queue = mLooper.getQueue(); + return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Access failed in TestableLooper: get - MessageQueue.mMessages", + e); } } |