Merge "[DO NOT MERGE] Bouncer - Update selected item text color" into tm-dev
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index acdab1b..ae16e01 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -3925,15 +3925,32 @@
* task will be moved to the back of the activity stack instead of being finished.
* Other activities will simply be finished.
*
- * <p>If you target version {@link android.os.Build.VERSION_CODES#S} or later and
- * override this method, it is strongly recommended to call through to the superclass
+ * <li><p>If you target version {@link android.os.Build.VERSION_CODES#S} and
+ * override this method, we strongly recommend to call through to the superclass
* implementation after you finish handling navigation within the app.
+ *
+ * <li><p>If you target version {@link android.os.Build.VERSION_CODES#TIRAMISU} or later,
+ * you should not use this method but register an {@link OnBackInvokedCallback} on an
+ * {@link OnBackInvokedDispatcher} that you can retrieve using
+ * {@link #getOnBackInvokedDispatcher()}. You should also set
+ * {@code android:enableOnBackInvokedCallback="true"} in the application manifest.
+ * <p>Alternatively, you can use
+ * {@code androidx.activity.ComponentActivity#getOnBackPressedDispatcher()}
+ * for backward compatibility.
* </ul>
*
* @see #moveTaskToBack(boolean)
*
* @deprecated Use {@link OnBackInvokedCallback} or
* {@code androidx.activity.OnBackPressedCallback} to handle back navigation instead.
+ * <p>
+ * Starting from Android 13 (API level 33), back event handling is
+ * moving to an ahead-of-time model and {@link Activity#onBackPressed()} and
+ * {@link KeyEvent#KEYCODE_BACK} should not be used to handle back events (back gesture or
+ * back button click). Instead, an {@link OnBackInvokedCallback} should be registered using
+ * {@link Activity#getOnBackInvokedDispatcher()}
+ * {@link OnBackInvokedDispatcher#registerOnBackInvokedCallback(int, OnBackInvokedCallback)
+ * .registerOnBackInvokedCallback(priority, callback)}.
*/
@Deprecated
public void onBackPressed() {
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index f0af9ba..33cf712 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -720,8 +720,27 @@
* key. The default implementation simply cancels the dialog (only if
* it is cancelable), but you can override this to do whatever you want.
*
+ * <p>
+ * If you target version {@link android.os.Build.VERSION_CODES#TIRAMISU} or later, you
+ * should not use this method but register an {@link OnBackInvokedCallback} on an
+ * {@link OnBackInvokedDispatcher} that you can retrieve using
+ * {@link #getOnBackInvokedDispatcher()}. You should also set
+ * {@code android:enableOnBackInvokedCallback="true"} in the application manifest.
+ *
+ * <p>Alternatively, you
+ * can use {@code androidx.activity.ComponentDialog#getOnBackPressedDispatcher()}
+ * for backward compatibility.
+ *
* @deprecated Use {@link OnBackInvokedCallback} or
* {@code androidx.activity.OnBackPressedCallback} to handle back navigation instead.
+ * <p>
+ * Starting from Android 13 (API level 33), back event handling is
+ * moving to an ahead-of-time model and {@link #onBackPressed()} and
+ * {@link KeyEvent#KEYCODE_BACK} should not be used to handle back events (back gesture or
+ * back button click). Instead, an {@link OnBackInvokedCallback} should be registered using
+ * {@link Dialog#getOnBackInvokedDispatcher()}
+ * {@link OnBackInvokedDispatcher#registerOnBackInvokedCallback(int, OnBackInvokedCallback)
+ * .registerOnBackInvokedCallback(priority, callback)}.
*/
@Deprecated
public void onBackPressed() {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 20a2bdf..1ef1ac5 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -15040,6 +15040,14 @@
*/
public static final String DEVICE_CONFIG_SYNC_DISABLED = "device_config_sync_disabled";
+
+ /**
+ * Whether back preview animations are played when user does a back gesture or presses
+ * the back button.
+ * @hide
+ */
+ public static final String ENABLE_BACK_ANIMATION = "enable_back_animation";
+
/** @hide */ public static String zenModeToString(int mode) {
if (mode == ZEN_MODE_IMPORTANT_INTERRUPTIONS) return "ZEN_MODE_IMPORTANT_INTERRUPTIONS";
if (mode == ZEN_MODE_ALARMS) return "ZEN_MODE_ALARMS";
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 8115eaf..82f8a13 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -81,9 +81,12 @@
@NonNull
private Consumer<List<SplitInfo>> mEmbeddingCallback;
private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
+ private final Handler mHandler;
public SplitController() {
- mPresenter = new SplitPresenter(new MainThreadExecutor(), this);
+ final MainThreadExecutor executor = new MainThreadExecutor();
+ mHandler = executor.mHandler;
+ mPresenter = new SplitPresenter(executor, this);
ActivityThread activityThread = ActivityThread.currentActivityThread();
// Register a callback to be notified about activities being created.
activityThread.getApplication().registerActivityLifecycleCallbacks(
@@ -167,11 +170,13 @@
// to fullscreen.
cleanupForEnterPip(wct, container);
mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
- } else {
+ } else if (taskFragmentInfo.isTaskClearedForReuse()) {
// Do not finish the dependents if this TaskFragment was cleared due to launching
// activity in the Task.
- final boolean shouldFinishDependent = !taskFragmentInfo.isTaskClearedForReuse();
- mPresenter.cleanupContainer(container, shouldFinishDependent, wct);
+ mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
+ } else if (!container.isWaitingActivityAppear()) {
+ // Do not finish the container before the expected activity appear until timeout.
+ mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct);
}
} else if (wasInPip && isInPip) {
// No update until exit PIP.
@@ -418,6 +423,14 @@
}
/**
+ * Called when we have been waiting too long for the TaskFragment to become non-empty after
+ * creation.
+ */
+ void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) {
+ mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+ }
+
+ /**
* Returns a container that this activity is registered with. An activity can only belong to one
* container, or no container at all.
*/
@@ -452,7 +465,7 @@
if (activityInTask == null) {
throw new IllegalArgumentException("activityInTask must not be null,");
}
- final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId);
+ final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId, this);
if (!mTaskContainers.contains(taskId)) {
mTaskContainers.put(taskId, new TaskContainer(taskId));
}
@@ -590,7 +603,12 @@
}
for (int i = taskContainer.mContainers.size() - 1; i >= 0; i--) {
final TaskFragmentContainer container = taskContainer.mContainers.get(i);
- if (!container.isFinished() && container.getRunningActivityCount() > 0) {
+ if (!container.isFinished() && (container.getRunningActivityCount() > 0
+ // We may be waiting for the top TaskFragment to become non-empty after
+ // creation. In that case, we don't want to treat the TaskFragment below it as
+ // top active, otherwise it may incorrectly launch placeholder on top of the
+ // pending TaskFragment.
+ || container.isWaitingActivityAppear())) {
return container;
}
}
@@ -920,6 +938,10 @@
return mTaskContainers.get(taskId);
}
+ Handler getHandler() {
+ return mHandler;
+ }
+
/**
* Returns {@code true} if an Activity with the provided component name should always be
* expanded to occupy full task bounds. Such activity must not be put in a split.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 03f38ed..35981d3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -30,6 +30,8 @@
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -39,6 +41,11 @@
* on the server side.
*/
class TaskFragmentContainer {
+ private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000;
+
+ @NonNull
+ private final SplitController mController;
+
/**
* Client-created token that uniquely identifies the task fragment container instance.
*/
@@ -51,7 +58,8 @@
/**
* Server-provided task fragment information.
*/
- private TaskFragmentInfo mInfo;
+ @VisibleForTesting
+ TaskFragmentInfo mInfo;
/**
* Activities that are being reparented or being started to this container, but haven't been
@@ -81,10 +89,20 @@
private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED;
/**
+ * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment
+ * if it is still empty after the timeout.
+ */
+ @VisibleForTesting
+ @Nullable
+ Runnable mAppearEmptyTimeout;
+
+ /**
* Creates a container with an existing activity that will be re-parented to it in a window
* container transaction.
*/
- TaskFragmentContainer(@Nullable Activity activity, int taskId) {
+ TaskFragmentContainer(@Nullable Activity activity, int taskId,
+ @NonNull SplitController controller) {
+ mController = controller;
mToken = new Binder("TaskFragmentContainer");
if (taskId == INVALID_TASK_ID) {
throw new IllegalArgumentException("Invalid Task id");
@@ -155,12 +173,30 @@
return count;
}
+ /** Whether we are waiting for the TaskFragment to appear and become non-empty. */
+ boolean isWaitingActivityAppear() {
+ return !mIsFinished && (mInfo == null || mAppearEmptyTimeout != null);
+ }
+
@Nullable
TaskFragmentInfo getInfo() {
return mInfo;
}
void setInfo(@NonNull TaskFragmentInfo info) {
+ if (!mIsFinished && mInfo == null && info.isEmpty()) {
+ // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is
+ // still empty after timeout.
+ mAppearEmptyTimeout = () -> {
+ mAppearEmptyTimeout = null;
+ mController.onTaskFragmentAppearEmptyTimeout(this);
+ };
+ mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
+ } else if (mAppearEmptyTimeout != null && !info.isEmpty()) {
+ mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
+ mAppearEmptyTimeout = null;
+ }
+
mInfo = info;
if (mInfo == null || mPendingAppearedActivities.isEmpty()) {
return;
@@ -234,6 +270,10 @@
@NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
if (!mIsFinished) {
mIsFinished = true;
+ if (mAppearEmptyTimeout != null) {
+ mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
+ mAppearEmptyTimeout = null;
+ }
finishActivities(shouldFinishDependent, presenter, wct, controller);
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index 1f12c448..7aa47ef 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -18,6 +18,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -29,6 +30,7 @@
import android.content.res.Configuration;
import android.graphics.Point;
+import android.os.Handler;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerToken;
@@ -61,6 +63,10 @@
private WindowContainerTransaction mTransaction;
@Mock
private JetpackTaskFragmentOrganizer.TaskFragmentCallback mCallback;
+ @Mock
+ private SplitController mSplitController;
+ @Mock
+ private Handler mHandler;
private JetpackTaskFragmentOrganizer mOrganizer;
@Before
@@ -69,6 +75,7 @@
mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback);
mOrganizer.registerOrganizer();
spyOn(mOrganizer);
+ doReturn(mHandler).when(mSplitController).getHandler();
}
@Test
@@ -106,7 +113,8 @@
@Test
public void testExpandTaskFragment() {
- final TaskFragmentContainer container = new TaskFragmentContainer(null, TASK_ID);
+ final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+ TASK_ID, mSplitController);
final TaskFragmentInfo info = createMockInfo(container);
mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
container.setInfo(info);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 32c61ea..983208c 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -35,6 +35,7 @@
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
+import android.os.Handler;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
@@ -48,6 +49,7 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -71,6 +73,9 @@
private TaskFragmentInfo mInfo;
@Mock
private WindowContainerTransaction mTransaction;
+ @Mock
+ private Handler mHandler;
+
private SplitController mSplitController;
private SplitPresenter mSplitPresenter;
@@ -86,6 +91,7 @@
activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
doReturn(mActivityResources).when(mActivity).getResources();
doReturn(activityConfig).when(mActivityResources).getConfiguration();
+ doReturn(mHandler).when(mSplitController).getHandler();
}
@Test
@@ -94,28 +100,45 @@
// tf3 is finished so is not active.
TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class);
doReturn(true).when(tf3).isFinished();
+ doReturn(false).when(tf3).isWaitingActivityAppear();
// tf2 has running activity so is active.
TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class);
doReturn(1).when(tf2).getRunningActivityCount();
// tf1 has no running activity so is not active.
- TaskFragmentContainer tf1 = new TaskFragmentContainer(null, TASK_ID);
+ TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, TASK_ID,
+ mSplitController);
- taskContainer.mContainers.add(tf3);
- taskContainer.mContainers.add(tf2);
taskContainer.mContainers.add(tf1);
+ taskContainer.mContainers.add(tf2);
+ taskContainer.mContainers.add(tf3);
mSplitController.mTaskContainers.put(TASK_ID, taskContainer);
assertWithMessage("Must return tf2 because tf3 is not active.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
- taskContainer.mContainers.remove(tf1);
+ taskContainer.mContainers.remove(tf3);
assertWithMessage("Must return tf2 because tf2 has running activity.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
taskContainer.mContainers.remove(tf2);
- assertWithMessage("Must return null because tf1 has no running activity.")
+ assertWithMessage("Must return tf because we are waiting for tf1 to appear.")
+ .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
+
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+ doReturn(new ArrayList<>()).when(info).getActivities();
+ doReturn(true).when(info).isEmpty();
+ tf1.setInfo(info);
+
+ assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after"
+ + " creation.")
+ .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
+
+ doReturn(false).when(info).isEmpty();
+ tf1.setInfo(info);
+
+ assertWithMessage("Must return null because tf1 becomes empty.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isNull();
}
@@ -133,6 +156,14 @@
}
@Test
+ public void testOnTaskFragmentAppearEmptyTimeout() {
+ final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
+ mSplitController.onTaskFragmentAppearEmptyTimeout(tf);
+
+ verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */);
+ }
+
+ @Test
public void testNewContainer() {
// Must pass in a valid activity.
assertThrows(IllegalArgumentException.class, () ->
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index c7feb7e..c40bab8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -32,8 +32,11 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
/**
* Test class for {@link TaskContainer}.
@@ -48,6 +51,14 @@
private static final int TASK_ID = 10;
private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
+ @Mock
+ private SplitController mController;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
@Test
public void testIsTaskBoundsInitialized() {
final TaskContainer taskContainer = new TaskContainer(TASK_ID);
@@ -126,7 +137,8 @@
assertTrue(taskContainer.isEmpty());
- final TaskFragmentContainer tf = new TaskFragmentContainer(null, TASK_ID);
+ final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, TASK_ID,
+ mController);
taskContainer.mContainers.add(tf);
assertFalse(taskContainer.isEmpty());
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 97896c2..d80f2b9 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -18,12 +18,18 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import android.app.Activity;
+import android.os.Handler;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
@@ -59,15 +65,19 @@
private Activity mActivity;
@Mock
private TaskFragmentInfo mInfo;
+ @Mock
+ private Handler mHandler;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+ doReturn(mHandler).when(mController).getHandler();
}
@Test
public void testFinish() {
- final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, TASK_ID,
+ mController);
final WindowContainerTransaction wct = new WindowContainerTransaction();
// Only remove the activity, but not clear the reference until appeared.
@@ -94,4 +104,62 @@
verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken());
verify(mController).removeContainer(container);
}
+
+ @Test
+ public void testIsWaitingActivityAppear() {
+ final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+ TASK_ID, mController);
+
+ assertTrue(container.isWaitingActivityAppear());
+
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+ doReturn(new ArrayList<>()).when(info).getActivities();
+ doReturn(true).when(info).isEmpty();
+ container.setInfo(info);
+
+ assertTrue(container.isWaitingActivityAppear());
+
+ doReturn(false).when(info).isEmpty();
+ container.setInfo(info);
+
+ assertFalse(container.isWaitingActivityAppear());
+ }
+
+ @Test
+ public void testAppearEmptyTimeout() {
+ final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+ TASK_ID, mController);
+
+ assertNull(container.mAppearEmptyTimeout);
+
+ // Not set if it is not appeared empty.
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+ doReturn(new ArrayList<>()).when(info).getActivities();
+ doReturn(false).when(info).isEmpty();
+ container.setInfo(info);
+
+ assertNull(container.mAppearEmptyTimeout);
+
+ // Set timeout if the first info set is empty.
+ container.mInfo = null;
+ doReturn(true).when(info).isEmpty();
+ container.setInfo(info);
+
+ assertNotNull(container.mAppearEmptyTimeout);
+
+ // Remove timeout after the container becomes non-empty.
+ doReturn(false).when(info).isEmpty();
+ container.setInfo(info);
+
+ assertNull(container.mAppearEmptyTimeout);
+
+ // Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout.
+ container.mInfo = null;
+ doReturn(true).when(info).isEmpty();
+ container.setInfo(info);
+ container.mAppearEmptyTimeout.run();
+
+ assertNull(container.mAppearEmptyTimeout);
+ verify(mController).onTaskFragmentAppearEmptyTimeout(container);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index ced36a7..c3fbe55 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -24,12 +24,18 @@
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.WindowConfiguration;
+import android.content.ContentResolver;
import android.content.Context;
+import android.database.ContentObserver;
import android.graphics.Point;
import android.graphics.PointF;
import android.hardware.HardwareBuffer;
+import android.net.Uri;
+import android.os.Handler;
import android.os.RemoteException;
import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings.Global;
import android.util.Log;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
@@ -42,22 +48,27 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
+import java.util.concurrent.atomic.AtomicBoolean;
+
/**
* Controls the window animation run when a user initiates a back gesture.
*/
public class BackAnimationController implements RemoteCallable<BackAnimationController> {
private static final String TAG = "BackAnimationController";
+ private static final int SETTING_VALUE_OFF = 0;
+ private static final int SETTING_VALUE_ON = 1;
private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
"persist.wm.debug.predictive_back_progress_threshold";
public static final boolean IS_ENABLED =
- SystemProperties.getInt("persist.wm.debug.predictive_back", 1) != 0;
+ SystemProperties.getInt("persist.wm.debug.predictive_back",
+ SETTING_VALUE_ON) != SETTING_VALUE_OFF;
private static final int PROGRESS_THRESHOLD = SystemProperties
.getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
- @VisibleForTesting
- boolean mEnableAnimations = SystemProperties.getInt(
- "persist.wm.debug.predictive_back_anim", 0) != 0;
+
+ private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
/**
* Location of the initial touch event of the back gesture.
@@ -87,21 +98,50 @@
private float mProgressThreshold;
public BackAnimationController(
- @ShellMainThread ShellExecutor shellExecutor,
+ @NonNull @ShellMainThread ShellExecutor shellExecutor,
+ @NonNull @ShellBackgroundThread Handler backgroundHandler,
Context context) {
- this(shellExecutor, new SurfaceControl.Transaction(), ActivityTaskManager.getService(),
- context);
+ this(shellExecutor, backgroundHandler, new SurfaceControl.Transaction(),
+ ActivityTaskManager.getService(), context, context.getContentResolver());
}
@VisibleForTesting
- BackAnimationController(@NonNull ShellExecutor shellExecutor,
+ BackAnimationController(@NonNull @ShellMainThread ShellExecutor shellExecutor,
+ @NonNull @ShellBackgroundThread Handler handler,
@NonNull SurfaceControl.Transaction transaction,
@NonNull IActivityTaskManager activityTaskManager,
- Context context) {
+ Context context, ContentResolver contentResolver) {
mShellExecutor = shellExecutor;
mTransaction = transaction;
mActivityTaskManager = activityTaskManager;
mContext = context;
+ setupAnimationDeveloperSettingsObserver(contentResolver, handler);
+ }
+
+ private void setupAnimationDeveloperSettingsObserver(
+ @NonNull ContentResolver contentResolver,
+ @NonNull @ShellBackgroundThread final Handler backgroundHandler) {
+ ContentObserver settingsObserver = new ContentObserver(backgroundHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ updateEnableAnimationFromSetting();
+ }
+ };
+ contentResolver.registerContentObserver(
+ Global.getUriFor(Global.ENABLE_BACK_ANIMATION),
+ false, settingsObserver, UserHandle.USER_SYSTEM
+ );
+ updateEnableAnimationFromSetting();
+ }
+
+ @ShellBackgroundThread
+ private void updateEnableAnimationFromSetting() {
+ int settingValue = Global.getInt(mContext.getContentResolver(),
+ Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF);
+ boolean isEnabled = settingValue == SETTING_VALUE_ON;
+ mEnableAnimations.set(isEnabled);
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s",
+ isEnabled);
}
public BackAnimation getBackAnimationImpl() {
@@ -340,12 +380,7 @@
private boolean shouldDispatchToLauncher(int backType) {
return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
&& mBackToLauncherCallback != null
- && mEnableAnimations;
- }
-
- @VisibleForTesting
- void setEnableAnimations(boolean shouldEnable) {
- mEnableAnimations = shouldEnable;
+ && mEnableAnimations.get();
}
private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 4ad0868..3335673 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -55,6 +55,7 @@
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ShellAnimationThread;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
import com.android.wm.shell.compatui.CompatUI;
@@ -734,11 +735,12 @@
@Provides
static Optional<BackAnimationController> provideBackAnimationController(
Context context,
- @ShellMainThread ShellExecutor shellExecutor
+ @ShellMainThread ShellExecutor shellExecutor,
+ @ShellBackgroundThread Handler backgroundHandler
) {
if (BackAnimationController.IS_ENABLED) {
return Optional.of(
- new BackAnimationController(shellExecutor, context));
+ new BackAnimationController(shellExecutor, backgroundHandler, context));
}
return Optional.empty();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index fb53e535..a899709 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -43,6 +43,7 @@
"truth-prebuilt",
"testables",
"platform-test-annotations",
+ "frameworks-base-testutils",
],
libs: [
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index a905dca..6cf8829 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -26,17 +26,23 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.IActivityTaskManager;
import android.app.WindowConfiguration;
-import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
+import android.os.Handler;
import android.os.RemoteCallback;
import android.os.RemoteException;
+import android.provider.Settings;
import android.testing.AndroidTestingRunner;
+import android.testing.TestableContentResolver;
+import android.testing.TestableContext;
+import android.testing.TestableLooper;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
@@ -45,12 +51,14 @@
import android.window.IOnBackInvokedCallback;
import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.internal.util.test.FakeSettingsProvider;
import com.android.wm.shell.TestShellExecutor;
-import com.android.wm.shell.common.ShellExecutor;
import org.junit.Before;
import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -60,14 +68,17 @@
/**
* atest WMShellUnitTests:BackAnimationControllerTest
*/
+@TestableLooper.RunWithLooper
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class BackAnimationControllerTest {
- private final ShellExecutor mShellExecutor = new TestShellExecutor();
+ private static final String ANIMATION_ENABLED = "1";
+ private final TestShellExecutor mShellExecutor = new TestShellExecutor();
- @Mock
- private Context mContext;
+ @Rule
+ public TestableContext mContext =
+ new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
@Mock
private SurfaceControl.Transaction mTransaction;
@@ -80,18 +91,32 @@
private BackAnimationController mController;
+ private int mEventTime = 0;
+ private TestableContentResolver mContentResolver;
+ private TestableLooper mTestableLooper;
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ mContext.getApplicationInfo().privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
+ mContentResolver = new TestableContentResolver(mContext);
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION,
+ ANIMATION_ENABLED);
+ mTestableLooper = TestableLooper.get(this);
mController = new BackAnimationController(
- mShellExecutor, mTransaction, mActivityTaskManager, mContext);
- mController.setEnableAnimations(true);
+ mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
+ mActivityTaskManager, mContext,
+ mContentResolver);
+ mEventTime = 0;
+ mShellExecutor.flushAll();
}
private void createNavigationInfo(RemoteAnimationTarget topAnimationTarget,
SurfaceControl screenshotSurface,
HardwareBuffer hardwareBuffer,
- int backType) {
+ int backType,
+ IOnBackInvokedCallback onBackInvokedCallback) {
BackNavigationInfo navigationInfo = new BackNavigationInfo(
backType,
topAnimationTarget,
@@ -99,7 +124,7 @@
hardwareBuffer,
new WindowConfiguration(),
new RemoteCallback((bundle) -> {}),
- null);
+ onBackInvokedCallback);
try {
doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation();
} catch (RemoteException ex) {
@@ -124,15 +149,10 @@
}
private void triggerBackGesture() {
- MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
- mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT);
-
- event = MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0);
- mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT);
-
+ doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+ doMotionEvent(MotionEvent.ACTION_MOVE, 0);
mController.setTriggerBack(true);
- event = MotionEvent.obtain(10, 0, MotionEvent.ACTION_UP, 100, 100, 0);
- mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT);
+ doMotionEvent(MotionEvent.ACTION_UP, 0);
}
@Test
@@ -141,11 +161,8 @@
SurfaceControl screenshotSurface = new SurfaceControl();
HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
createNavigationInfo(createAnimationTarget(), screenshotSurface, hardwareBuffer,
- BackNavigationInfo.TYPE_CROSS_ACTIVITY);
- mController.onMotionEvent(
- MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0),
- MotionEvent.ACTION_DOWN,
- BackEvent.EDGE_LEFT);
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY, null);
+ doMotionEvent(MotionEvent.ACTION_DOWN, 0);
verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer);
verify(mTransaction).setVisibility(screenshotSurface, true);
verify(mTransaction).apply();
@@ -157,15 +174,9 @@
HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
RemoteAnimationTarget animationTarget = createAnimationTarget();
createNavigationInfo(animationTarget, screenshotSurface, hardwareBuffer,
- BackNavigationInfo.TYPE_CROSS_ACTIVITY);
- mController.onMotionEvent(
- MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0),
- MotionEvent.ACTION_DOWN,
- BackEvent.EDGE_LEFT);
- mController.onMotionEvent(
- MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0),
- MotionEvent.ACTION_MOVE,
- BackEvent.EDGE_LEFT);
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY, null);
+ doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+ doMotionEvent(MotionEvent.ACTION_MOVE, 100);
// b/207481538, we check that the surface is not moved for now, we can re-enable this once
// we implement the animation
verify(mTransaction, never()).setScale(eq(screenshotSurface), anyInt(), anyInt());
@@ -196,30 +207,56 @@
mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
RemoteAnimationTarget animationTarget = createAnimationTarget();
createNavigationInfo(animationTarget, null, null,
- BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
// Check that back start is dispatched.
- mController.onMotionEvent(
- MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0),
- MotionEvent.ACTION_DOWN,
- BackEvent.EDGE_LEFT);
+ doMotionEvent(MotionEvent.ACTION_DOWN, 0);
verify(mIOnBackInvokedCallback).onBackStarted();
// Check that back progress is dispatched.
- mController.onMotionEvent(
- MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0),
- MotionEvent.ACTION_MOVE,
- BackEvent.EDGE_LEFT);
+ doMotionEvent(MotionEvent.ACTION_MOVE, 100);
ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
verify(mIOnBackInvokedCallback).onBackProgressed(backEventCaptor.capture());
assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget());
// Check that back invocation is dispatched.
mController.setTriggerBack(true); // Fake trigger back
- mController.onMotionEvent(
- MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0),
- MotionEvent.ACTION_UP,
- BackEvent.EDGE_LEFT);
+ doMotionEvent(MotionEvent.ACTION_UP, 0);
verify(mIOnBackInvokedCallback).onBackInvoked();
}
+
+ @Test
+ public void animationDisabledFromSettings() throws RemoteException {
+ // Toggle the setting off
+ Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0");
+ mController = new BackAnimationController(
+ mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
+ mActivityTaskManager, mContext,
+ mContentResolver);
+ mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
+
+ RemoteAnimationTarget animationTarget = createAnimationTarget();
+ IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
+ ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+ createNavigationInfo(animationTarget, null, null,
+ BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback);
+
+ triggerBackGesture();
+
+ verify(appCallback, never()).onBackStarted();
+ verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
+ verify(appCallback, times(1)).onBackInvoked();
+
+ verify(mIOnBackInvokedCallback, never()).onBackStarted();
+ verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
+ verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+ }
+
+ private void doMotionEvent(int actionDown, int coordinate) {
+ mController.onMotionEvent(
+ MotionEvent.obtain(0, mEventTime, actionDown, coordinate, coordinate, 0),
+ actionDown,
+ BackEvent.EDGE_LEFT);
+ mEventTime += 10;
+ }
}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index df2685d..a171f86 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1604,4 +1604,11 @@
<string name="bt_le_audio_broadcast_dialog_switch_app">Broadcast <xliff:g id="switchApp" example="App Name 2">%1$s</xliff:g></string>
<!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, different output. -->
<string name="bt_le_audio_broadcast_dialog_different_output">Change output</string>
+
+ <!-- Developer setting: enable animations when a back gesture is executed [CHAR LIMIT=50] -->
+ <string name="back_navigation_animation">Predictive back animations</string>
+ <!-- Developer setting: enable animations when a back gesture is executed [CHAR LIMIT=150] -->
+ <string name="back_navigation_animation_summary">Enable system animations for predictive back.</string>
+ <!-- Developer setting: enable animations when a back gesture is executed, full explanation[CHAR LIMIT=NONE] -->
+ <string name="back_navigation_animation_dialog">This setting enables system animations for predictive gesture animation. It requires setting per-app "enableOnBackInvokedCallback" to true in the manifest file.</string>
</resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index e1a2e8d..d6d7304 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -46,8 +46,6 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -56,7 +54,6 @@
*/
@RequiresApi(Build.VERSION_CODES.R)
public class LocalMediaManager implements BluetoothCallback {
- private static final Comparator<MediaDevice> COMPARATOR = Comparator.naturalOrder();
private static final String TAG = "LocalMediaManager";
private static final int MAX_DISCONNECTED_DEVICE_NUM = 5;
@@ -65,13 +62,15 @@
MediaDeviceState.STATE_CONNECTING,
MediaDeviceState.STATE_DISCONNECTED,
MediaDeviceState.STATE_CONNECTING_FAILED,
- MediaDeviceState.STATE_SELECTED})
+ MediaDeviceState.STATE_SELECTED,
+ MediaDeviceState.STATE_GROUPING})
public @interface MediaDeviceState {
int STATE_CONNECTED = 0;
int STATE_CONNECTING = 1;
int STATE_DISCONNECTED = 2;
int STATE_CONNECTING_FAILED = 3;
int STATE_SELECTED = 4;
+ int STATE_GROUPING = 5;
}
private final Collection<DeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
@@ -322,6 +321,7 @@
* @return If add device successful return {@code true}, otherwise return {@code false}
*/
public boolean addDeviceToPlayMedia(MediaDevice device) {
+ device.setState(MediaDeviceState.STATE_GROUPING);
return mInfoMediaManager.addDeviceToPlayMedia(device);
}
@@ -332,6 +332,7 @@
* @return If device stop successful return {@code true}, otherwise return {@code false}
*/
public boolean removeDeviceFromPlayMedia(MediaDevice device) {
+ device.setState(MediaDeviceState.STATE_GROUPING);
return mInfoMediaManager.removeDeviceFromPlayMedia(device);
}
@@ -524,7 +525,6 @@
@Override
public void onDeviceListAdded(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
- Collections.sort(devices, COMPARATOR);
mMediaDevices.clear();
mMediaDevices.addAll(devices);
// Add disconnected bluetooth devices only when phone output device is available.
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 4b7d0d2..cce5154 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -598,6 +598,7 @@
Settings.Global.WATCHDOG_TIMEOUT_MILLIS,
Settings.Global.MANAGED_PROVISIONING_DEFER_PROVISIONING_TO_ROLE_HOLDER,
Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ Settings.Global.ENABLE_BACK_ANIMATION, // Temporary for T, dev option only
Settings.Global.Wearable.BATTERY_SAVER_MODE,
Settings.Global.Wearable.COMBINED_LOCATION_ENABLED,
Settings.Global.Wearable.HAS_PAY_TOKENS,
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
index e1b99ce..55dce8f 100644
--- a/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
+++ b/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
@@ -27,7 +27,7 @@
<clip>
<shape>
<corners
- android:radius="28dp"/>
+ android:radius="16dp"/>
<size
android:height="64dp"/>
<solid android:color="@color/material_dynamic_primary80" />
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
index dc86afa..f79e534 100644
--- a/packages/SystemUI/res/layout/media_output_list_item.xml
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -33,12 +33,13 @@
android:layout_height="match_parent"
android:background="@drawable/media_output_item_background"
android:layout_gravity="center_vertical|start">
- <SeekBar
+ <com.android.systemui.media.dialog.MediaOutputSeekbar
android:id="@+id/volume_seekbar"
android:splitTrack="false"
android:visibility="gone"
android:paddingStart="0dp"
android:paddingEnd="0dp"
+ android:background="@null"
android:progressDrawable="@drawable/media_output_dialog_seekbar_background"
android:thumb="@null"
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 4370432..f77430b 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1125,6 +1125,8 @@
<dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
<dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
<dimen name="media_output_dialog_app_tier_icon_size">20dp</dimen>
+ <dimen name="media_output_dialog_background_radius">16dp</dimen>
+ <dimen name="media_output_dialog_active_background_radius">28dp</dimen>
<!-- Distance that the full shade transition takes in order to complete by tapping on a button
like "expand". -->
diff --git a/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
new file mode 100644
index 0000000..f195d20
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_WAKE
+import com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.SecureSettings
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Handles active unlock settings changes.
+ */
+@SysUISingleton
+class ActiveUnlockConfig @Inject constructor(
+ @Main private val handler: Handler,
+ private val secureSettings: SecureSettings,
+ private val contentResolver: ContentResolver,
+ dumpManager: DumpManager
+) : Dumpable {
+
+ /**
+ * Indicates the origin for an active unlock request.
+ */
+ enum class ACTIVE_UNLOCK_REQUEST_ORIGIN {
+ WAKE, UNLOCK_INTENT, BIOMETRIC_FAIL, ASSISTANT
+ }
+
+ private var requestActiveUnlockOnWakeup = false
+ private var requestActiveUnlockOnUnlockIntent = false
+ private var requestActiveUnlockOnBioFail = false
+
+ private val settingsObserver = object : ContentObserver(handler) {
+ private val wakeUri: Uri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_WAKE)
+ private val unlockIntentUri: Uri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_UNLOCK_INTENT)
+ private val bioFailUri: Uri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL)
+
+ fun register() {
+ contentResolver.registerContentObserver(
+ wakeUri,
+ false,
+ this,
+ UserHandle.USER_ALL)
+ contentResolver.registerContentObserver(
+ unlockIntentUri,
+ false,
+ this,
+ UserHandle.USER_ALL)
+ contentResolver.registerContentObserver(
+ bioFailUri,
+ false,
+ this,
+ UserHandle.USER_ALL)
+
+ onChange(true, ArrayList(), 0, getCurrentUser())
+ }
+
+ override fun onChange(
+ selfChange: Boolean,
+ uris: Collection<Uri>,
+ flags: Int,
+ userId: Int
+ ) {
+ if (getCurrentUser() != userId) {
+ return
+ }
+
+ if (selfChange || uris.contains(wakeUri)) {
+ requestActiveUnlockOnWakeup = secureSettings.getIntForUser(
+ ACTIVE_UNLOCK_ON_WAKE, 0, getCurrentUser()) == 1
+ }
+
+ if (selfChange || uris.contains(unlockIntentUri)) {
+ requestActiveUnlockOnUnlockIntent = secureSettings.getIntForUser(
+ ACTIVE_UNLOCK_ON_UNLOCK_INTENT, 0, getCurrentUser()) == 1
+ }
+
+ if (selfChange || uris.contains(bioFailUri)) {
+ requestActiveUnlockOnBioFail = secureSettings.getIntForUser(
+ ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, 0, getCurrentUser()) == 1
+ }
+ }
+ }
+
+ init {
+ settingsObserver.register()
+ dumpManager.registerDumpable(this)
+ }
+
+ /**
+ * Whether to trigger active unlock based on where the request is coming from and
+ * the current settings.
+ */
+ fun shouldAllowActiveUnlockFromOrigin(requestOrigin: ACTIVE_UNLOCK_REQUEST_ORIGIN): Boolean {
+ return when (requestOrigin) {
+ ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE -> requestActiveUnlockOnWakeup
+
+ ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT ->
+ requestActiveUnlockOnUnlockIntent || requestActiveUnlockOnWakeup
+
+ ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL ->
+ requestActiveUnlockOnBioFail || requestActiveUnlockOnUnlockIntent ||
+ requestActiveUnlockOnWakeup
+
+ ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT -> isActiveUnlockEnabled()
+ }
+ }
+
+ /**
+ * If any active unlock triggers are enabled.
+ */
+ fun isActiveUnlockEnabled(): Boolean {
+ return requestActiveUnlockOnWakeup || requestActiveUnlockOnUnlockIntent ||
+ requestActiveUnlockOnBioFail
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println(" requestActiveUnlockOnWakeup=$requestActiveUnlockOnWakeup")
+ pw.println(" requestActiveUnlockOnUnlockIntent=$requestActiveUnlockOnUnlockIntent")
+ pw.println(" requestActiveUnlockOnBioFail=$requestActiveUnlockOnBioFail")
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 90f53a1..19a2d9e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -221,10 +221,10 @@
mKeyguardSecurityCallback.userActivity();
showMessage(null, null);
}
- if (mUpdateMonitor.isFaceEnrolled()
- && mUpdateMonitor.mRequestActiveUnlockOnUnlockIntent) {
- mUpdateMonitor.requestActiveUnlock("unlock-intent, reason=swipeUpOnBouncer",
- true);
+ if (mUpdateMonitor.isFaceEnrolled()) {
+ mUpdateMonitor.requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ "swipeUpOnBouncer");
}
}
};
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index a6feedb5..bbe9a362 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -248,12 +248,6 @@
}
}
- public final boolean mRequestActiveUnlockOnAssistant;
- public final boolean mRequestActiveUnlockOnWakeup;
- public final boolean mInitiateActiveUnlockOnWakeup;
- public final boolean mRequestActiveUnlockOnUnlockIntent;
- public final boolean mRequestActiveUnlockOnBioFail;
-
private final Context mContext;
private final boolean mIsPrimaryUser;
private final boolean mIsAutomotive;
@@ -340,6 +334,7 @@
private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private final Executor mBackgroundExecutor;
private SensorPrivacyManager mSensorPrivacyManager;
+ private final ActiveUnlockConfig mActiveUnlockConfig;
/**
* Short delay before restarting fingerprint authentication after a successful try. This should
@@ -441,12 +436,12 @@
Assert.isMainThread();
boolean wasTrusted = mUserHasTrust.get(userId, false);
mUserHasTrust.put(userId, enabled);
- // If there was no change in trusted state, make sure we are not authenticating.
- // TrustManager sends an onTrustChanged whenever a user unlocks keyguard, for
- // this reason we need to make sure to not authenticate.
- if (wasTrusted == enabled) {
+ // If there was no change in trusted state or trust granted, make sure we are not
+ // authenticating. TrustManager sends an onTrustChanged whenever a user unlocks keyguard,
+ // for this reason we need to make sure to not authenticate.
+ if (wasTrusted == enabled || enabled) {
updateBiometricListeningState(BIOMETRIC_ACTION_STOP);
- } else if (!enabled) {
+ } else {
updateBiometricListeningState(BIOMETRIC_ACTION_START);
}
@@ -1364,14 +1359,18 @@
cb.onTrustAgentErrorMessage(message);
}
}
+
}
@VisibleForTesting
void setAssistantVisible(boolean assistantVisible) {
mAssistantVisible = assistantVisible;
updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
- if (mAssistantVisible && mRequestActiveUnlockOnAssistant) {
- requestActiveUnlock("assistant", false);
+ if (mAssistantVisible) {
+ requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT,
+ "assistant",
+ false);
}
}
@@ -1518,10 +1517,9 @@
@Override
public void onAuthenticationFailed() {
- if (mRequestActiveUnlockOnBioFail) {
- requestActiveUnlock("biometric-failure, extra=fingerprintFailure",
- true);
- }
+ requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ "fingerprintFailure");
handleFingerprintAuthFailed();
}
@@ -1580,14 +1578,13 @@
@Override
public void onAuthenticationFailed() {
- if (shouldRequestActiveUnlockOnFaceError()) {
String reason =
mKeyguardBypassController.canBypass() ? "bypass"
: mUdfpsBouncerShowing ? "udfpsBouncer" :
mBouncerFullyShown ? "bouncer" : "udfpsFpDown";
- requestActiveUnlock("biometric-failure"
- + ", extra=faceFailure-" + reason, true);
- }
+ requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ "faceFailure-" + reason);
handleFaceAuthFailed();
if (mKeyguardBypassController != null) {
@@ -1617,10 +1614,11 @@
if (mKeyguardBypassController != null) {
mKeyguardBypassController.setUserHasDeviceEntryIntent(false);
}
- if (errMsgId == BiometricFaceConstants.FACE_ERROR_TIMEOUT
- && shouldRequestActiveUnlockOnFaceError()) {
- requestActiveUnlock("biometric-failure"
- + ", extra=faceError-" + errMsgId, true);
+
+ if (errMsgId == BiometricFaceConstants.FACE_ERROR_TIMEOUT) {
+ requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ "faceError-" + errMsgId);
}
}
@@ -1628,12 +1626,6 @@
public void onAuthenticationAcquired(int acquireInfo) {
handleFaceAcquired(acquireInfo);
}
-
- private boolean shouldRequestActiveUnlockOnFaceError() {
- return mRequestActiveUnlockOnBioFail
- && (mKeyguardBypassController.canBypass() || mBouncerFullyShown
- || mUdfpsBouncerShowing || mAuthController.isUdfpsFingerDown());
- }
};
@VisibleForTesting
@@ -1749,11 +1741,7 @@
Trace.beginSection("KeyguardUpdateMonitor#handleStartedWakingUp");
Assert.isMainThread();
updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
- if (mRequestActiveUnlockOnWakeup) {
- requestActiveUnlock("wake-unlock");
- } else if (mInitiateActiveUnlockOnWakeup) {
- initiateActiveUnlock("wake-initiate");
- }
+ requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp");
for (int i = 0; i < mCallbacks.size(); i++) {
KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
if (cb != null) {
@@ -1882,7 +1870,8 @@
AuthController authController,
TelephonyListenerManager telephonyListenerManager,
InteractionJankMonitor interactionJankMonitor,
- LatencyTracker latencyTracker) {
+ LatencyTracker latencyTracker,
+ ActiveUnlockConfig activeUnlockConfiguration) {
mContext = context;
mSubscriptionManager = SubscriptionManager.from(context);
mTelephonyListenerManager = telephonyListenerManager;
@@ -1899,18 +1888,7 @@
mAuthController = authController;
dumpManager.registerDumpable(getClass().getName(), this);
mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
-
- // TODO, b/222459888: add official configurable names to Settings.java
- mRequestActiveUnlockOnWakeup = Settings.Global.getInt(
- mContext.getContentResolver(), "wake-unlock", 0) == 1;
- mInitiateActiveUnlockOnWakeup = Settings.Global.getInt(
- mContext.getContentResolver(), "wake-initiate", 1) == 1;
- mRequestActiveUnlockOnUnlockIntent = Settings.Global.getInt(
- mContext.getContentResolver(), "unlock-intent", 0) == 1;
- mRequestActiveUnlockOnBioFail = Settings.Global.getInt(
- mContext.getContentResolver(), "bio-fail", 0) == 1;
- mRequestActiveUnlockOnAssistant = Settings.Global.getInt(
- mContext.getContentResolver(), "assistant", 0) == 1;
+ mActiveUnlockConfig = activeUnlockConfiguration;
mHandler = new Handler(mainLooper) {
@Override
@@ -2292,11 +2270,7 @@
}
mAuthInterruptActive = active;
updateFaceListeningState(BIOMETRIC_ACTION_UPDATE);
- if (mRequestActiveUnlockOnWakeup) {
- requestActiveUnlock("wake-unlock, extra=onReach");
- } else if (mInitiateActiveUnlockOnWakeup) {
- initiateActiveUnlock("wake-initiate, extra=onReach");
- }
+ requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "onReach");
}
/**
@@ -2348,7 +2322,7 @@
/**
* Initiates active unlock to get the unlock token ready.
*/
- public void initiateActiveUnlock(String reason) {
+ private void initiateActiveUnlock(String reason) {
// If this message exists, FP has already authenticated, so wait until that is handled
if (mHandler.hasMessages(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE)) {
return;
@@ -2365,15 +2339,30 @@
/**
* Attempts to trigger active unlock from trust agent.
*/
- public void requestActiveUnlock(String reason, boolean dismissKeyguard) {
+ private void requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+ String reason,
+ boolean dismissKeyguard
+ ) {
// If this message exists, FP has already authenticated, so wait until that is handled
if (mHandler.hasMessages(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE)) {
return;
}
- if (shouldTriggerActiveUnlock()) {
+ final boolean allowRequest =
+ mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(requestOrigin);
+ if (requestOrigin == ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE
+ && !allowRequest && mActiveUnlockConfig.isActiveUnlockEnabled()) {
+ // instead of requesting the active unlock, initiate the unlock
+ initiateActiveUnlock(reason);
+ return;
+ }
+
+ if (allowRequest && shouldTriggerActiveUnlock()) {
if (DEBUG) {
- Log.d("ActiveUnlock", "reportUserRequestedUnlock triggerReason=" + reason
+ Log.d("ActiveUnlock", "reportUserRequestedUnlock"
+ + " origin=" + requestOrigin.name()
+ + " reason=" + reason
+ " dismissKeyguard=" + dismissKeyguard);
}
mTrustManager.reportUserRequestedUnlock(KeyguardUpdateMonitor.getCurrentUser(),
@@ -2383,11 +2372,20 @@
/**
* Attempts to trigger active unlock from trust agent.
- * Only dismisses the keyguard if only face is enrolled (no FP) and bypass is enabled.
+ * Only dismisses the keyguard under certain conditions.
*/
- public void requestActiveUnlock(String reason) {
- requestActiveUnlock(reason, isFaceEnrolled() && !isUdfpsEnrolled()
- && mKeyguardBypassController.getBypassEnabled());
+ public void requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+ String extraReason
+ ) {
+ final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null
+ && mKeyguardBypassController.canBypass();
+ requestActiveUnlock(
+ requestOrigin,
+ extraReason, canFaceBypass
+ || mUdfpsBouncerShowing
+ || mBouncerFullyShown
+ || mAuthController.isUdfpsFingerDown());
}
/**
@@ -2397,9 +2395,9 @@
mUdfpsBouncerShowing = showing;
if (mUdfpsBouncerShowing) {
updateFaceListeningState(BIOMETRIC_ACTION_START);
- if (mRequestActiveUnlockOnUnlockIntent) {
- requestActiveUnlock("unlock-intent, extra=udfpsBouncer", true);
- }
+ requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ "udfpsBouncer");
}
}
@@ -3233,8 +3231,10 @@
}
if (wasBouncerFullyShown != mBouncerFullyShown) {
- if (mBouncerFullyShown && mRequestActiveUnlockOnUnlockIntent) {
- requestActiveUnlock("unlock-intent, reason=bouncerFullyShown", true);
+ if (mBouncerFullyShown) {
+ requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ "bouncerFullyShown");
}
for (int i = 0; i < mCallbacks.size(); i++) {
KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
@@ -3771,13 +3771,6 @@
}
mListenModels.print(pw);
- pw.println("Enabled active unlock triggers:");
- pw.println(" mRequestActiveUnlockOnWakeup=" + mRequestActiveUnlockOnWakeup);
- pw.println(" mInitiateActiveUnlockOnWakeup=" + mInitiateActiveUnlockOnWakeup);
- pw.println(" mRequestActiveUnlockOnUnlockIntent=" + mRequestActiveUnlockOnUnlockIntent);
- pw.println(" mRequestActiveUnlockOnBiometricFail=" + mRequestActiveUnlockOnBioFail);
- pw.println(" mRequestActiveUnlockOnAssistant=" + mRequestActiveUnlockOnAssistant);
-
if (mIsAutomotive) {
pw.println(" Running on Automotive build");
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 20efafc..463db5c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -51,6 +51,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.ActiveUnlockConfig;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.dagger.SysUISingleton;
@@ -658,9 +659,15 @@
mExecution.assertIsMainThread();
mOverlay = overlay;
+ final int requestReason = overlay.getRequestReason();
+ if (requestReason == REASON_AUTH_KEYGUARD
+ && !mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
+ Log.d(TAG, "Attempting to showUdfpsOverlay when fingerprint detection"
+ + " isn't running on keyguard. Skip show.");
+ return;
+ }
if (overlay.show(this, mOverlayParams)) {
- Log.v(TAG, "showUdfpsOverlay | adding window reason="
- + overlay.getRequestReason());
+ Log.v(TAG, "showUdfpsOverlay | adding window reason=" + requestReason);
mOnFingerDown = false;
mAttemptedToDismissKeyguard = false;
mOrientationListener.enable();
@@ -791,10 +798,9 @@
mKeyguardUpdateMonitor.requestFaceAuth(/* userInitiatedRequest */ false);
}
- if (mKeyguardUpdateMonitor.mRequestActiveUnlockOnUnlockIntent) {
- mKeyguardUpdateMonitor.requestActiveUnlock("unlock-intent extra=udfpsFingerDown",
- true);
- }
+ mKeyguardUpdateMonitor.requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ "udfpsFingerDown");
}
mOnFingerDown = true;
if (mAlternateTouchProvider != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 07001ee..a397f32 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -112,6 +112,7 @@
super.onBind(device, topMargin, bottomMargin, position);
final boolean currentlyConnected = !mIncludeDynamicGroup
&& isCurrentlyConnected(device);
+ boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
if (currentlyConnected) {
mConnectedItem = mContainerLayout;
}
@@ -165,6 +166,14 @@
true /* showSubtitle */, true /* showStatus */);
mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+ } else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
+ mProgressBar.getIndeterminateDrawable().setColorFilter(
+ new PorterDuffColorFilter(
+ mController.getColorItemContent(),
+ PorterDuff.Mode.SRC_IN));
+ setSingleLineLayout(getItemTitle(device), true /* bFocused */,
+ false /* showSeekBar*/,
+ true /* showProgressBar */, false /* showStatus */);
} else if (mController.getSelectedMediaDevice().size() > 1
&& isDeviceIncluded(mController.getSelectedMediaDevice(), device)) {
mTitleText.setTextColor(mController.getColorItemContent());
@@ -178,7 +187,7 @@
mCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> onGroupActionTriggered(false, device));
setCheckBoxColor(mCheckBox, mController.getColorItemContent());
- initSeekbar(device);
+ initSeekbar(device, isCurrentSeekbarInvisible);
mEndTouchArea.setVisibility(View.VISIBLE);
mEndTouchArea.setOnClickListener(null);
mEndTouchArea.setOnClickListener((v) -> mCheckBox.performClick());
@@ -193,7 +202,7 @@
setSingleLineLayout(getItemTitle(device), true /* bFocused */,
true /* showSeekBar */,
false /* showProgressBar */, true /* showStatus */);
- initSeekbar(device);
+ initSeekbar(device, isCurrentSeekbarInvisible);
setUpContentDescriptionForView(mContainerLayout, false, device);
mCurrentActivePosition = position;
} else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
@@ -257,9 +266,7 @@
mCurrentActivePosition = -1;
mController.connectDevice(device);
device.setState(MediaDeviceState.STATE_CONNECTING);
- if (!isAnimating()) {
- notifyDataSetChanged();
- }
+ notifyDataSetChanged();
}
private void setUpContentDescriptionForView(View view, boolean clickable,
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 9dc29bd..5c2cc0b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -17,18 +17,22 @@
package com.android.systemui.media.dialog;
import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.app.WallpaperColors;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Typeface;
+import android.graphics.drawable.ClipDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.Icon;
+import android.graphics.drawable.LayerDrawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.animation.LinearInterpolator;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.ImageView;
@@ -44,7 +48,6 @@
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
import java.util.List;
@@ -61,7 +64,6 @@
protected final MediaOutputController mController;
private int mMargin;
- private boolean mIsAnimating;
Context mContext;
View mHolderView;
@@ -114,10 +116,6 @@
return mIsDragging;
}
- boolean isAnimating() {
- return mIsAnimating;
- }
-
int getCurrentActivePosition() {
return mCurrentActivePosition;
}
@@ -131,7 +129,7 @@
*/
abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
- private static final int ANIM_DURATION = 200;
+ private static final int ANIM_DURATION = 500;
final LinearLayout mContainerLayout;
final FrameLayout mItemLayout;
@@ -140,12 +138,14 @@
final TextView mSubTitleText;
final ImageView mTitleIcon;
final ProgressBar mProgressBar;
- final SeekBar mSeekBar;
+ final MediaOutputSeekbar mSeekBar;
final LinearLayout mTwoLineLayout;
final ImageView mStatusIcon;
final CheckBox mCheckBox;
final LinearLayout mEndTouchArea;
private String mDeviceId;
+ private ValueAnimator mCornerAnimator;
+ private ValueAnimator mVolumeAnimator;
MediaDeviceBaseViewHolder(View view) {
super(view);
@@ -161,6 +161,7 @@
mStatusIcon = view.requireViewById(R.id.media_output_item_status);
mCheckBox = view.requireViewById(R.id.check_box);
mEndTouchArea = view.requireViewById(R.id.end_action_area);
+ initAnimator();
}
void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) {
@@ -186,20 +187,39 @@
boolean showProgressBar, boolean showStatus) {
mTwoLineLayout.setVisibility(View.GONE);
boolean isActive = showSeekBar || showProgressBar;
- final Drawable backgroundDrawable =
- isActive
- ? mContext.getDrawable(R.drawable.media_output_item_background_active)
- .mutate() : mContext.getDrawable(
- R.drawable.media_output_item_background)
- .mutate();
- backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
- isActive ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
- mItemLayout.setBackground(backgroundDrawable);
+ if (!mCornerAnimator.isRunning()) {
+ final Drawable backgroundDrawable =
+ showSeekBar
+ ? mContext.getDrawable(
+ R.drawable.media_output_item_background_active)
+ .mutate() : mContext.getDrawable(
+ R.drawable.media_output_item_background)
+ .mutate();
+ backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
+ isActive ? mController.getColorConnectedItemBackground()
+ : mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
+ mItemLayout.setBackground(backgroundDrawable);
+ if (showSeekBar) {
+ final ClipDrawable clipDrawable =
+ (ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
+ .findDrawableByLayerId(android.R.id.progress);
+ final GradientDrawable progressDrawable =
+ (GradientDrawable) clipDrawable.getDrawable();
+ progressDrawable.setCornerRadius(mController.getActiveRadius());
+ }
+ } else {
+ mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
+ isActive ? mController.getColorConnectedItemBackground()
+ : mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
+ }
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
mSeekBar.setAlpha(1);
mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
+ if (!showSeekBar) {
+ mSeekBar.resetVolume();
+ }
mStatusIcon.setVisibility(showStatus ? View.VISIBLE : View.GONE);
mTitleText.setText(title);
mTitleText.setVisibility(View.VISIBLE);
@@ -257,15 +277,21 @@
}
}
- void initSeekbar(MediaDevice device) {
+ void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) {
if (!mController.isVolumeControlEnabled(device)) {
disableSeekBar();
}
- mSeekBar.setMax(device.getMaxVolume());
- mSeekBar.setMin(0);
+ mSeekBar.setMaxVolume(device.getMaxVolume());
final int currentVolume = device.getCurrentVolume();
- if (mSeekBar.getProgress() != currentVolume) {
- mSeekBar.setProgress(currentVolume, true);
+ if (mSeekBar.getVolume() != currentVolume) {
+ if (isCurrentSeekbarInvisible) {
+ animateCornerAndVolume(mSeekBar.getProgress(),
+ MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
+ } else {
+ if (!mVolumeAnimator.isStarted()) {
+ mSeekBar.setVolume(currentVolume);
+ }
+ }
}
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
@@ -273,7 +299,11 @@
if (device == null || !fromUser) {
return;
}
- mController.adjustVolume(device, progress);
+ int currentVolume = MediaOutputSeekbar.scaleProgressToVolume(progress);
+ int deviceVolume = device.getCurrentVolume();
+ if (currentVolume != deviceVolume) {
+ mController.adjustVolume(device, currentVolume);
+ }
}
@Override
@@ -317,65 +347,57 @@
});
}
- void playSwitchingAnim(@NonNull View from, @NonNull View to) {
- final float delta = (float) (mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_title_anim_y_delta));
- final SeekBar fromSeekBar = from.requireViewById(R.id.volume_seekbar);
- final TextView toTitleText = to.requireViewById(R.id.title);
- if (fromSeekBar.getVisibility() != View.VISIBLE || toTitleText.getVisibility()
- != View.VISIBLE) {
- return;
- }
- mIsAnimating = true;
- // Animation for title text
- toTitleText.setTypeface(Typeface.create(mContext.getString(
- com.android.internal.R.string.config_headlineFontFamilyMedium),
- Typeface.NORMAL));
- toTitleText.animate()
- .setDuration(ANIM_DURATION)
- .translationY(-delta)
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- to.requireViewById(R.id.volume_indeterminate_progress).setVisibility(
- View.VISIBLE);
- // Unset the listener, otherwise this may persist for another view
- // property animation
- toTitleText.animate().setListener(null);
- }
- });
- // Animation for seek bar
- fromSeekBar.animate()
- .alpha(0)
- .setDuration(ANIM_DURATION)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- final TextView fromTitleText = from.requireViewById(
- R.id.two_line_title);
- fromTitleText.setTypeface(Typeface.create(mContext.getString(
- com.android.internal.R.string.config_headlineFontFamily),
- Typeface.NORMAL));
- fromTitleText.animate()
- .setDuration(ANIM_DURATION)
- .translationY(delta)
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mIsAnimating = false;
- notifyDataSetChanged();
- // Unset the listener, otherwise this may persist for
- // another view property animation
- fromTitleText.animate().setListener(null);
- }
- });
- // Unset the listener, otherwise this may persist for another view
- // property animation
- fromSeekBar.animate().setListener(null);
- }
- });
+ private void animateCornerAndVolume(int fromProgress, int toProgress) {
+ final GradientDrawable layoutBackgroundDrawable =
+ (GradientDrawable) mItemLayout.getBackground();
+ final ClipDrawable clipDrawable =
+ (ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
+ .findDrawableByLayerId(android.R.id.progress);
+ final GradientDrawable progressDrawable = (GradientDrawable) clipDrawable.getDrawable();
+ mCornerAnimator.addUpdateListener(animation -> {
+ float value = (float) animation.getAnimatedValue();
+ layoutBackgroundDrawable.setCornerRadius(value);
+ progressDrawable.setCornerRadius(value);
+ });
+ mVolumeAnimator.setIntValues(fromProgress, toProgress);
+ mVolumeAnimator.start();
+ mCornerAnimator.start();
+ }
+
+ private void initAnimator() {
+ mCornerAnimator = ValueAnimator.ofFloat(mController.getInactiveRadius(),
+ mController.getActiveRadius());
+ mCornerAnimator.setDuration(ANIM_DURATION);
+ mCornerAnimator.setInterpolator(new LinearInterpolator());
+
+ mVolumeAnimator = ValueAnimator.ofInt();
+ mVolumeAnimator.addUpdateListener(animation -> {
+ int value = (int) animation.getAnimatedValue();
+ mSeekBar.setProgress(value);
+ });
+ mVolumeAnimator.setDuration(ANIM_DURATION);
+ mVolumeAnimator.setInterpolator(new LinearInterpolator());
+ mVolumeAnimator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mSeekBar.setEnabled(false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mSeekBar.setEnabled(true);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mSeekBar.setEnabled(true);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+ });
}
Drawable getSpeakerDrawable() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index e5e7eb6..5bb6557 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -184,6 +184,19 @@
}
};
+ private class LayoutManagerWrapper extends LinearLayoutManager {
+ LayoutManagerWrapper(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
+ super.onLayoutCompleted(state);
+ mMediaOutputController.setRefreshing(false);
+ mMediaOutputController.refreshDataSetIfNeeded();
+ }
+ }
+
public MediaOutputBaseDialog(Context context, BroadcastSender broadcastSender,
MediaOutputController mediaOutputController) {
super(context, R.style.Theme_SystemUI_Dialog_Media);
@@ -192,7 +205,7 @@
mContext = getContext();
mBroadcastSender = broadcastSender;
mMediaOutputController = mediaOutputController;
- mLayoutManager = new LinearLayoutManager(mContext);
+ mLayoutManager = new LayoutManagerWrapper(mContext);
mListMaxHeight = context.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_list_max_height);
mExecutor = Executors.newSingleThreadExecutor();
@@ -274,6 +287,10 @@
}
void refresh(boolean deviceSetChanged) {
+ if (mMediaOutputController.isRefreshing()) {
+ return;
+ }
+ mMediaOutputController.setRefreshing(true);
// Update header icon
final int iconRes = getHeaderIconRes();
final IconCompat iconCompat = getHeaderIcon();
@@ -334,7 +351,7 @@
mHeaderSubtitle.setText(subTitle);
mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
}
- if (!mAdapter.isDragging() && !mAdapter.isAnimating()) {
+ if (!mAdapter.isDragging()) {
int currentActivePosition = mAdapter.getCurrentActivePosition();
if (!colorSetUpdated && !deviceSetChanged && currentActivePosition >= 0
&& currentActivePosition < mAdapter.getItemCount()) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 8723f4f..e7f97d2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -81,6 +81,8 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -108,11 +110,15 @@
private final DialogLaunchAnimator mDialogLaunchAnimator;
private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
private final CommonNotifCollection mNotifCollection;
+ private final Object mMediaDevicesLock = new Object();
@VisibleForTesting
final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
+ final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
+ private boolean mIsRefreshing = false;
+ private boolean mNeedRefresh = false;
private MediaController mMediaController;
@VisibleForTesting
Callback mCallback;
@@ -127,6 +133,8 @@
private int mColorItemBackground;
private int mColorConnectedItemBackground;
private int mColorPositiveButtonText;
+ private float mInactiveRadius;
+ private float mActiveRadius;
public enum BroadcastNotifyDialog {
ACTION_FIRST_LAUNCH,
@@ -163,10 +171,17 @@
R.color.media_dialog_connected_item_background);
mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext,
R.color.media_dialog_solid_button_text);
+ mInactiveRadius = mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_background_radius);
+ mActiveRadius = mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_active_background_radius);
}
void start(@NonNull Callback cb) {
- mMediaDevices.clear();
+ synchronized (mMediaDevicesLock) {
+ mCachedMediaDevices.clear();
+ mMediaDevices.clear();
+ }
mNearbyDeviceInfoMap.clear();
if (mNearbyMediaDevicesManager != null) {
mNearbyMediaDevicesManager.registerNearbyDevicesCallback(this);
@@ -205,6 +220,14 @@
return routerParams != null && !routerParams.isMediaTransferReceiverEnabled();
}
+ void setRefreshing(boolean refreshing) {
+ mIsRefreshing = refreshing;
+ }
+
+ boolean isRefreshing() {
+ return mIsRefreshing;
+ }
+
void stop() {
if (mMediaController != null) {
mMediaController.unregisterCallback(mCb);
@@ -213,7 +236,10 @@
mLocalMediaManager.unregisterCallback(this);
mLocalMediaManager.stopScan();
}
- mMediaDevices.clear();
+ synchronized (mMediaDevicesLock) {
+ mCachedMediaDevices.clear();
+ mMediaDevices.clear();
+ }
if (mNearbyMediaDevicesManager != null) {
mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
}
@@ -222,15 +248,23 @@
@Override
public void onDeviceListUpdate(List<MediaDevice> devices) {
- buildMediaDevices(devices);
- mCallback.onDeviceListChanged();
+ if (mMediaDevices.isEmpty() || !mIsRefreshing) {
+ buildMediaDevices(devices);
+ mCallback.onDeviceListChanged();
+ } else {
+ synchronized (mMediaDevicesLock) {
+ mNeedRefresh = true;
+ mCachedMediaDevices.clear();
+ mCachedMediaDevices.addAll(devices);
+ }
+ }
}
@Override
public void onSelectedDeviceStateChanged(MediaDevice device,
@LocalMediaManager.MediaDeviceState int state) {
mCallback.onRouteChanged();
- mMetricLogger.logOutputSuccess(device.toString(), mMediaDevices);
+ mMetricLogger.logOutputSuccess(device.toString(), new ArrayList<>(mMediaDevices));
}
@Override
@@ -241,7 +275,7 @@
@Override
public void onRequestFailed(int reason) {
mCallback.onRouteChanged();
- mMetricLogger.logOutputFailure(mMediaDevices, reason);
+ mMetricLogger.logOutputFailure(new ArrayList<>(mMediaDevices), reason);
}
Drawable getAppSourceIcon() {
@@ -393,6 +427,14 @@
}
}
+ void refreshDataSetIfNeeded() {
+ if (mNeedRefresh) {
+ buildMediaDevices(mCachedMediaDevices);
+ mCallback.onDeviceListChanged();
+ mNeedRefresh = false;
+ }
+ }
+
public int getColorConnectedItemBackground() {
return mColorConnectedItemBackground;
}
@@ -417,51 +459,64 @@
return mColorItemBackground;
}
- private void buildMediaDevices(List<MediaDevice> devices) {
- // For the first time building list, to make sure the top device is the connected device.
- if (mMediaDevices.isEmpty()) {
- final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
- if (connectedMediaDevice == null) {
- if (DEBUG) {
- Log.d(TAG, "No connected media device.");
- }
- mMediaDevices.addAll(devices);
- return;
- }
- for (MediaDevice device : devices) {
- if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
- mMediaDevices.add(0, device);
- } else {
- mMediaDevices.add(device);
- }
- }
- return;
- }
- // To keep the same list order
- final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
- for (MediaDevice originalDevice : mMediaDevices) {
- for (MediaDevice newDevice : devices) {
- if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
- targetMediaDevices.add(newDevice);
- break;
- }
- }
- }
- if (targetMediaDevices.size() != devices.size()) {
- devices.removeAll(targetMediaDevices);
- targetMediaDevices.addAll(devices);
- }
- mMediaDevices.clear();
- mMediaDevices.addAll(targetMediaDevices);
- attachRangeInfo();
+ public float getInactiveRadius() {
+ return mInactiveRadius;
}
- private void attachRangeInfo() {
- for (MediaDevice mediaDevice : mMediaDevices) {
+ public float getActiveRadius() {
+ return mActiveRadius;
+ }
+
+ private void buildMediaDevices(List<MediaDevice> devices) {
+ synchronized (mMediaDevicesLock) {
+ attachRangeInfo(devices);
+ Collections.sort(devices, Comparator.naturalOrder());
+ // For the first time building list, to make sure the top device is the connected
+ // device.
+ if (mMediaDevices.isEmpty()) {
+ final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
+ if (connectedMediaDevice == null) {
+ if (DEBUG) {
+ Log.d(TAG, "No connected media device.");
+ }
+ mMediaDevices.addAll(devices);
+ return;
+ }
+ for (MediaDevice device : devices) {
+ if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
+ mMediaDevices.add(0, device);
+ } else {
+ mMediaDevices.add(device);
+ }
+ }
+ return;
+ }
+ // To keep the same list order
+ final List<MediaDevice> targetMediaDevices = new ArrayList<>();
+ for (MediaDevice originalDevice : mMediaDevices) {
+ for (MediaDevice newDevice : devices) {
+ if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
+ targetMediaDevices.add(newDevice);
+ break;
+ }
+ }
+ }
+ if (targetMediaDevices.size() != devices.size()) {
+ devices.removeAll(targetMediaDevices);
+ targetMediaDevices.addAll(devices);
+ }
+ mMediaDevices.clear();
+ mMediaDevices.addAll(targetMediaDevices);
+ }
+ }
+
+ private void attachRangeInfo(List<MediaDevice> devices) {
+ for (MediaDevice mediaDevice : devices) {
if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) {
mediaDevice.setRangeZone(mNearbyDeviceInfoMap.get(mediaDevice.getId()));
}
}
+
}
List<MediaDevice> getGroupMediaDevices() {
@@ -595,26 +650,30 @@
}
boolean isTransferring() {
- for (MediaDevice device : mMediaDevices) {
- if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
- return true;
+ synchronized (mMediaDevicesLock) {
+ for (MediaDevice device : mMediaDevices) {
+ if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
+ return true;
+ }
}
}
return false;
}
boolean isZeroMode() {
- if (mMediaDevices.size() == 1) {
- final MediaDevice device = mMediaDevices.iterator().next();
- // Add "pair new" only when local output device exists
- final int type = device.getDeviceType();
- if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
- || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
- || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
- return true;
+ synchronized (mMediaDevicesLock) {
+ if (mMediaDevices.size() == 1) {
+ final MediaDevice device = mMediaDevices.iterator().next();
+ // Add "pair new" only when local output device exists
+ final int type = device.getDeviceType();
+ if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
+ || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
+ || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
+ return true;
+ }
}
+ return false;
}
- return false;
}
void launchBluetoothPairing(View view) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
index 9b42b1d..ba2f006 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
@@ -101,9 +101,10 @@
mCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
onCheckBoxClicked(isChecked, device);
});
+ boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
setTwoLineLayout(device, false /* bFocused */, true /* showSeekBar */,
false /* showProgressBar */, false /* showSubtitle*/);
- initSeekbar(device);
+ initSeekbar(device, isCurrentSeekbarInvisible);
final List<MediaDevice> selectedDevices = mController.getSelectedMediaDevice();
if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
mCheckBox.setButtonDrawable(R.drawable.ic_check_box);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java
new file mode 100644
index 0000000..4ff79d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+/**
+ * Customized SeekBar for MediaOutputDialog, apply scale between device volume and progress, to make
+ * adjustment smoother.
+ */
+public class MediaOutputSeekbar extends SeekBar {
+ private static final int SCALE_SIZE = 1000;
+
+ public MediaOutputSeekbar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setMin(0);
+ }
+
+ static int scaleProgressToVolume(int progress) {
+ return progress / SCALE_SIZE;
+ }
+
+ static int scaleVolumeToProgress(int volume) {
+ return volume * SCALE_SIZE;
+ }
+
+ int getVolume() {
+ return getProgress() / SCALE_SIZE;
+ }
+
+ void setVolume(int volume) {
+ setProgress(volume * SCALE_SIZE, true);
+ }
+
+ void setMaxVolume(int maxVolume) {
+ setMax(maxVolume * SCALE_SIZE);
+ }
+
+ void resetVolume() {
+ setProgress(getMin());
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index ce6f3e4..6602f99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -510,7 +510,8 @@
.setMessage(trustGrantedIndication)
.setTextColor(mInitialTextColorState)
.build(),
- false);
+ true);
+ hideBiometricMessage();
} else if (!TextUtils.isEmpty(trustManagedIndication)
&& mKeyguardUpdateMonitor.getUserTrustIsManaged(userId)
&& !userHasTrust) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index 3e32b64..3f8e97f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -21,6 +21,7 @@
import android.hardware.Sensor
import android.hardware.TriggerEvent
import android.hardware.TriggerEventListener
+import com.android.keyguard.ActiveUnlockConfig
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.CoreStartable
@@ -71,13 +72,9 @@
isListening = false
updateListeningState()
keyguardUpdateMonitor.requestFaceAuth(true)
- if (keyguardUpdateMonitor.mRequestActiveUnlockOnWakeup) {
- keyguardUpdateMonitor.requestActiveUnlock("wake-unlock," +
- " extra=KeyguardLiftController")
- } else if (keyguardUpdateMonitor.mInitiateActiveUnlockOnWakeup) {
- keyguardUpdateMonitor.initiateActiveUnlock("wake-initiate," +
- " extra=KeyguardLiftController")
- }
+ keyguardUpdateMonitor.requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE,
+ "KeyguardLiftController")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 98a711d..b5789fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -98,6 +98,7 @@
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.policy.SystemBarUtils;
import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.ActiveUnlockConfig;
import com.android.keyguard.KeyguardStatusView;
import com.android.keyguard.KeyguardStatusViewController;
import com.android.keyguard.KeyguardUnfoldTransition;
@@ -3383,11 +3384,10 @@
.log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT);
startUnlockHintAnimation();
}
- if (mUpdateMonitor.isFaceEnrolled()
- && mUpdateMonitor.mRequestActiveUnlockOnUnlockIntent
- && mKeyguardBypassController.canBypass()) {
- mUpdateMonitor.requestActiveUnlock("unlock-intent,"
- + " extra=lockScreenEmptySpaceTap", true);
+ if (mUpdateMonitor.isFaceEnrolled()) {
+ mUpdateMonitor.requestActiveUnlock(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ "lockScreenEmptySpaceTap");
}
}
return true;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
new file mode 100644
index 0000000..7476490
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.settings.SecureSettings
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class ActiveUnlockConfigTest : SysuiTestCase() {
+ private val fakeWakeUri = Uri.Builder().appendPath("wake").build()
+ private val fakeUnlockIntentUri = Uri.Builder().appendPath("unlock-intent").build()
+ private val fakeBioFailUri = Uri.Builder().appendPath("bio-fail").build()
+
+ @Mock
+ private lateinit var secureSettings: SecureSettings
+
+ @Mock
+ private lateinit var contentResolver: ContentResolver
+
+ @Mock
+ private lateinit var handler: Handler
+
+ @Mock
+ private lateinit var dumpManager: DumpManager
+
+ @Captor
+ private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
+
+ private lateinit var activeUnlockConfig: ActiveUnlockConfig
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_WAKE))
+ .thenReturn(fakeWakeUri)
+ `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT))
+ .thenReturn(fakeUnlockIntentUri)
+ `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
+ .thenReturn(fakeBioFailUri)
+
+ activeUnlockConfig = ActiveUnlockConfig(
+ handler,
+ secureSettings,
+ contentResolver,
+ dumpManager
+ )
+ }
+
+ @Test
+ fun testRegsitersForSettingsChanges() {
+ verifyRegisterSettingObserver()
+ }
+
+ @Test
+ fun testOnWakeupSettingChanged() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN no active unlock settings enabled
+ assertFalse(
+ activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+ )
+
+ // WHEN unlock on wake is allowed
+ `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_WAKE,
+ 0, 0)).thenReturn(1)
+ settingsObserverCaptor.value.onChange(
+ false,
+ listOf(fakeWakeUri),
+ 0,
+ 0
+ )
+
+ // THEN active unlock triggers allowed on: wake, unlock-intent, and biometric failure
+ assertTrue(
+ activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+ )
+ assertTrue(
+ activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+ )
+ assertTrue(
+ activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)
+ )
+ }
+
+ @Test
+ fun testOnUnlockIntentSettingChanged() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN no active unlock settings enabled
+ assertFalse(
+ activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+ )
+
+ // WHEN unlock on biometric failed is allowed
+ `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT,
+ 0, 0)).thenReturn(1)
+ settingsObserverCaptor.value.onChange(
+ false,
+ listOf(fakeUnlockIntentUri),
+ 0,
+ 0
+ )
+
+ // THEN active unlock triggers allowed on: biometric failure ONLY
+ assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+ assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ }
+
+ @Test
+ fun testOnBioFailSettingChanged() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN no active unlock settings enabled
+ assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+
+ // WHEN unlock on biometric failed is allowed
+ `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
+ 0, 0)).thenReturn(1)
+ settingsObserverCaptor.value.onChange(
+ false,
+ listOf(fakeBioFailUri),
+ 0,
+ 0
+ )
+
+ // THEN active unlock triggers allowed on: biometric failure ONLY
+ assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+ assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+ ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ }
+
+ private fun verifyRegisterSettingObserver() {
+ verify(contentResolver).registerContentObserver(
+ eq(fakeWakeUri),
+ eq(false),
+ capture(settingsObserverCaptor),
+ eq(UserHandle.USER_ALL))
+
+ verify(contentResolver).registerContentObserver(
+ eq(fakeUnlockIntentUri),
+ eq(false),
+ capture(settingsObserverCaptor),
+ eq(UserHandle.USER_ALL))
+
+ verify(contentResolver).registerContentObserver(
+ eq(fakeBioFailUri),
+ eq(false),
+ capture(settingsObserverCaptor),
+ eq(UserHandle.USER_ALL))
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 86a4f5a..2dc066c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -177,6 +177,8 @@
private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateListenerCaptor;
@Mock
private KeyguardUpdateMonitorCallback mTestCallback;
+ @Mock
+ private ActiveUnlockConfig mActiveUnlockConfig;
// Direct executor
private Executor mBackgroundExecutor = Runnable::run;
private Executor mMainExecutor = Runnable::run;
@@ -1188,7 +1190,7 @@
mBackgroundExecutor, mMainExecutor,
mStatusBarStateController, mLockPatternUtils,
mAuthController, mTelephonyListenerManager,
- mInteractionJankMonitor, mLatencyTracker);
+ mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig);
setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 7779f42..da5939a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -212,6 +212,7 @@
.thenReturn(mFpmOtherView);
when(mEnrollView.getContext()).thenReturn(mContext);
when(mKeyguardStateController.isOccluded()).thenReturn(false);
+ when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
final List<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
index cf6fd24..9256cd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
@@ -185,25 +185,14 @@
}
@Test
- public void onBindViewHolder_verifySessionVolume() {
- when(mMediaOutputController.getSessionVolume()).thenReturn(TEST_VOLUME);
- when(mMediaOutputController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME);
-
- mGroupAdapter.onBindViewHolder(mGroupViewHolder, 0);
-
- assertThat(mGroupViewHolder.mSeekBar.getProgress()).isEqualTo(TEST_VOLUME);
- assertThat(mGroupViewHolder.mSeekBar.getMax()).isEqualTo(TEST_MAX_VOLUME);
- }
-
- @Test
public void onBindViewHolder_verifyDeviceVolume() {
when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_VOLUME);
when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
+ mGroupViewHolder.mSeekBar.setVisibility(View.VISIBLE);
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
- assertThat(mGroupViewHolder.mSeekBar.getProgress()).isEqualTo(TEST_VOLUME);
- assertThat(mGroupViewHolder.mSeekBar.getMax()).isEqualTo(TEST_MAX_VOLUME);
+ assertThat(mGroupViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_VOLUME);
}
@Test
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 0b3c18b..3b715a2 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -82,6 +82,11 @@
/** ID for Communication strategy retrieved form audio policy manager */
private int mCommunicationStrategyId = -1;
+
+ /** ID for Accessibility strategy retrieved form audio policy manager */
+ private int mAccessibilityStrategyId = -1;
+
+
/** Active communication device reported by audio policy manager */
private AudioDeviceInfo mActiveCommunicationDevice;
/** Last preferred device set for communication strategy */
@@ -141,22 +146,28 @@
init();
}
- private void initCommunicationStrategyId() {
+ private void initRoutingStrategyIds() {
List<AudioProductStrategy> strategies = AudioProductStrategy.getAudioProductStrategies();
+ mCommunicationStrategyId = -1;
+ mAccessibilityStrategyId = -1;
for (AudioProductStrategy strategy : strategies) {
- if (strategy.getAudioAttributesForLegacyStreamType(AudioSystem.STREAM_VOICE_CALL)
- != null) {
+ if (mCommunicationStrategyId == -1
+ && strategy.getAudioAttributesForLegacyStreamType(
+ AudioSystem.STREAM_VOICE_CALL) != null) {
mCommunicationStrategyId = strategy.getId();
- return;
+ }
+ if (mAccessibilityStrategyId == -1
+ && strategy.getAudioAttributesForLegacyStreamType(
+ AudioSystem.STREAM_ACCESSIBILITY) != null) {
+ mAccessibilityStrategyId = strategy.getId();
}
}
- mCommunicationStrategyId = -1;
}
private void init() {
setupMessaging(mContext);
- initCommunicationStrategyId();
+ initRoutingStrategyIds();
mPreferredCommunicationDevice = null;
updateActiveCommunicationDevice();
@@ -813,19 +824,10 @@
return mDeviceInventory.setPreferredDevicesForStrategySync(strategy, devices);
}
- /*package*/ void postSetPreferredDevicesForStrategy(int strategy,
- @NonNull List<AudioDeviceAttributes> devices) {
- sendILMsgNoDelay(MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY, SENDMSG_REPLACE, strategy, devices);
- }
-
/*package*/ int removePreferredDevicesForStrategySync(int strategy) {
return mDeviceInventory.removePreferredDevicesForStrategySync(strategy);
}
- /*package*/ void postRemovePreferredDevicesForStrategy(int strategy) {
- sendIMsgNoDelay(MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_REPLACE, strategy);
- }
-
/*package*/ void registerStrategyPreferredDevicesDispatcher(
@NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher);
@@ -1157,6 +1159,9 @@
pw.println(prefix + "mCommunicationStrategyId: "
+ mCommunicationStrategyId);
+ pw.println(prefix + "mAccessibilityStrategyId: "
+ + mAccessibilityStrategyId);
+
pw.println("\n" + prefix + "mModeOwnerPid: " + mModeOwnerPid);
mBtHelper.dump(pw, prefix);
@@ -1252,7 +1257,7 @@
case MSG_RESTORE_DEVICES:
synchronized (mSetModeLock) {
synchronized (mDeviceStateLock) {
- initCommunicationStrategyId();
+ initRoutingStrategyIds();
updateActiveCommunicationDevice();
mDeviceInventory.onRestoreDevices();
mBtHelper.onAudioServerDiedRestoreA2dp();
@@ -1440,22 +1445,6 @@
final int strategy = msg.arg1;
mDeviceInventory.onSaveRemovePreferredDevices(strategy);
} break;
- case MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY: {
- final int strategy = msg.arg1;
- final List<AudioDeviceAttributes> devices =
- (List<AudioDeviceAttributes>) msg.obj;
- setPreferredDevicesForStrategySync(strategy, devices);
- if (strategy == mCommunicationStrategyId) {
- onUpdatePhoneStrategyDevice(devices.isEmpty() ? null : devices.get(0));
- }
- } break;
- case MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY: {
- final int strategy = msg.arg1;
- removePreferredDevicesForStrategySync(strategy);
- if (strategy == mCommunicationStrategyId) {
- onUpdatePhoneStrategyDevice(null);
- }
- } break;
case MSG_CHECK_MUTE_MUSIC:
checkMessagesMuteMusic(0);
break;
@@ -1533,8 +1522,6 @@
private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38;
private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE = 39;
- private static final int MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY = 40;
- private static final int MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY = 41;
private static final int MSG_L_SET_COMMUNICATION_ROUTE_FOR_CLIENT = 42;
private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
private static final int MSG_I_SCO_AUDIO_STATE_CHANGED = 44;
@@ -1836,9 +1823,12 @@
}
if (preferredCommunicationDevice == null) {
removePreferredDevicesForStrategySync(mCommunicationStrategyId);
+ removePreferredDevicesForStrategySync(mAccessibilityStrategyId);
} else {
setPreferredDevicesForStrategySync(
mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice));
+ setPreferredDevicesForStrategySync(
+ mAccessibilityStrategyId, Arrays.asList(preferredCommunicationDevice));
}
onUpdatePhoneStrategyDevice(preferredCommunicationDevice);
}
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 6a78739..47e606a 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -181,7 +181,7 @@
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment info changed name=%s",
tf.getName());
try {
- mOrganizer.onTaskFragmentInfoChanged(tf.getTaskFragmentInfo());
+ mOrganizer.onTaskFragmentInfoChanged(info);
mLastSentTaskFragmentInfos.put(tf, info);
} catch (RemoteException e) {
Slog.d(TAG, "Exception sending onTaskFragmentInfoChanged callback", e);
@@ -424,6 +424,10 @@
}
// Remove and add for re-ordering.
mPendingTaskFragmentEvents.remove(pendingEvent);
+ // Reset the defer time when TaskFragment is changed, so that it can check again if
+ // the event should be sent to the organizer, for example the TaskFragment may become
+ // empty.
+ pendingEvent.mDeferTime = 0;
}
mPendingTaskFragmentEvents.add(pendingEvent);
}
@@ -654,26 +658,15 @@
return null;
}
- private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task,
- @NonNull PendingTaskFragmentEvent event) {
+ private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) {
final TaskFragmentOrganizerState state =
mTaskFragmentOrganizerState.get(event.mTaskFragmentOrg.asBinder());
final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos.get(event.mTaskFragment);
final TaskFragmentInfo info = event.mTaskFragment.getTaskFragmentInfo();
// Send an info changed callback if this event is for the last activities to finish in a
- // Task so that the {@link TaskFragmentOrganizer} can delete this TaskFragment. Otherwise,
- // the Task may be removed before it becomes visible again to send this event because it no
- // longer has activities. As a result, the organizer will never get this info changed event
- // and will not delete the TaskFragment because the organizer thinks the TaskFragment still
- // has running activities.
- // Another case is when an organized TaskFragment became empty because the last running
- // activity is reparented to a new Task due to enter PiP. We also want to notify the
- // organizer, so it can remove the empty TaskFragment and update the paired TaskFragment
- // without causing the extra delay.
+ // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment.
return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED
- && (task.topRunningActivity() == null || info.isTaskFragmentClearedForPip())
- && lastInfo != null
- && lastInfo.getRunningActivityCount() > 0 && info.getRunningActivityCount() == 0;
+ && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty();
}
void dispatchPendingEvents() {
@@ -690,7 +683,7 @@
final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null;
if (task != null && (task.lastActiveTime <= event.mDeferTime
|| !(isTaskVisible(task, visibleTasks, invisibleTasks)
- || shouldSendEventWhenTaskInvisible(task, event)))) {
+ || shouldSendEventWhenTaskInvisible(event)))) {
// Defer sending events to the TaskFragment until the host task is active again.
event.mDeferTime = task.lastActiveTime;
continue;
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 24d04da..75b5e73 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -779,6 +779,47 @@
}
/**
+ * Tests that a task fragment info changed event is sent if the TaskFragment becomes empty
+ * even if the Task is invisible.
+ */
+ @Test
+ public void testPendingTaskFragmentInfoChangedEvent_emptyTaskFragment() {
+ // Create a TaskFragment with an activity, all within a parent task
+ final Task task = createTask(mDisplayContent);
+ final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .setOrganizer(mOrganizer)
+ .setFragmentToken(mFragmentToken)
+ .createActivityCount(1)
+ .build();
+ final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
+ // Add another activity in the Task so that it always contains a non-finishing activitiy.
+ final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+ assertTrue(task.shouldBeVisible(null));
+
+ // Dispatch pending info changed event from creating the activity
+ mController.registerOrganizer(mIOrganizer);
+ taskFragment.mTaskFragmentAppearedSent = true;
+ mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+ mController.dispatchPendingEvents();
+ verify(mOrganizer).onTaskFragmentInfoChanged(any());
+
+ // Verify the info changed callback is not called when the task is invisible
+ reset(mOrganizer);
+ doReturn(false).when(task).shouldBeVisible(any());
+ mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+ mController.dispatchPendingEvents();
+ verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+
+ // Finish the embedded activity, and verify the info changed callback is called because the
+ // TaskFragment is becoming empty.
+ embeddedActivity.finishing = true;
+ mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+ mController.dispatchPendingEvents();
+ verify(mOrganizer).onTaskFragmentInfoChanged(any());
+ }
+
+ /**
* When an embedded {@link TaskFragment} is removed, we should clean up the reference in the
* {@link WindowOrganizerController}.
*/