summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt7
-rw-r--r--core/api/system-current.txt4
-rw-r--r--core/java/android/app/ActivityThread.java18
-rw-r--r--core/java/android/app/UiAutomation.java2
-rw-r--r--core/java/android/hardware/biometrics/BiometricConstants.java1
-rw-r--r--core/java/android/hardware/biometrics/BiometricManager.java2
-rw-r--r--core/java/android/hardware/biometrics/flags.aconfig8
-rw-r--r--core/java/android/os/TestLooperManager.java31
-rw-r--r--core/java/android/view/ViewRootImpl.java3
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureManager.java11
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureSession.java3
-rw-r--r--core/java/android/view/contentcapture/MainContentCaptureSession.java119
-rw-r--r--core/java/android/view/contentcapture/flags/content_capture_flags.aconfig11
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig7
-rw-r--r--core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt2
-rw-r--r--core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java53
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig7
-rw-r--r--libs/WindowManager/Shell/res/values/ids.xml5
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt95
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt101
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt63
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java3
-rw-r--r--media/java/android/media/AudioFormat.java8
-rw-r--r--media/java/android/media/audio/common/AidlConversion.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt152
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java27
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java58
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt33
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt39
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt1
-rw-r--r--services/core/java/com/android/server/biometrics/AuthService.java7
-rw-r--r--services/core/java/com/android/server/biometrics/BiometricService.java5
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerSettings.java46
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java144
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java17
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java13
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java6
-rw-r--r--telephony/java/android/telephony/TelephonyManager.java2
-rw-r--r--tests/testables/src/android/testing/TestableLooper.java52
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);
}
}