Merge "Make new headers flag true by default" into tm-qpr-dev
diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java
index 794c694..c5dedb3 100644
--- a/core/java/android/app/LocaleManager.java
+++ b/core/java/android/app/LocaleManager.java
@@ -173,7 +173,7 @@
@TestApi
public void setSystemLocales(@NonNull LocaleList locales) {
try {
- Configuration conf = ActivityManager.getService().getConfiguration();
+ Configuration conf = new Configuration();
conf.setLocales(locales);
ActivityManager.getService().updatePersistentConfiguration(conf);
} catch (RemoteException e) {
diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java
index 3c53d07..7dd1d26 100644
--- a/core/java/com/android/internal/app/LocalePicker.java
+++ b/core/java/com/android/internal/app/LocalePicker.java
@@ -311,8 +311,7 @@
try {
final IActivityManager am = ActivityManager.getService();
- final Configuration config = am.getConfiguration();
-
+ final Configuration config = new Configuration();
config.setLocales(locales);
config.userSetLocale = true;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 00be5a6e..77284c41 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -109,6 +109,12 @@
return (mSplitRule instanceof SplitPlaceholderRule);
}
+ @NonNull
+ SplitInfo toSplitInfo() {
+ return new SplitInfo(mPrimaryContainer.toActivityStack(),
+ mSecondaryContainer.toActivityStack(), mSplitAttributes);
+ }
+
static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule)
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 bf7326a..1d513e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -1422,6 +1422,11 @@
@GuardedBy("mLock")
void updateContainer(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) {
+ if (!container.getTaskContainer().isVisible()) {
+ // Wait until the Task is visible to avoid unnecessary update when the Task is still in
+ // background.
+ return;
+ }
if (launchPlaceholderIfNecessary(wct, container)) {
// Placeholder was launched, the positions will be updated when the activity is added
// to the secondary container.
@@ -1643,16 +1648,14 @@
/**
* Notifies listeners about changes to split states if necessary.
*/
+ @VisibleForTesting
@GuardedBy("mLock")
- private void updateCallbackIfNecessary() {
- if (mEmbeddingCallback == null) {
+ void updateCallbackIfNecessary() {
+ if (mEmbeddingCallback == null || !readyToReportToClient()) {
return;
}
- if (!allActivitiesCreated()) {
- return;
- }
- List<SplitInfo> currentSplitStates = getActiveSplitStates();
- if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) {
+ final List<SplitInfo> currentSplitStates = getActiveSplitStates();
+ if (mLastReportedSplitStates.equals(currentSplitStates)) {
return;
}
mLastReportedSplitStates.clear();
@@ -1661,48 +1664,27 @@
}
/**
- * @return a list of descriptors for currently active split states. If the value returned is
- * null, that indicates that the active split states are in an intermediate state and should
- * not be reported.
+ * Returns a list of descriptors for currently active split states.
*/
@GuardedBy("mLock")
- @Nullable
+ @NonNull
private List<SplitInfo> getActiveSplitStates() {
- List<SplitInfo> splitStates = new ArrayList<>();
+ final List<SplitInfo> splitStates = new ArrayList<>();
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i)
- .mSplitContainers;
- for (SplitContainer container : splitContainers) {
- if (container.getPrimaryContainer().isEmpty()
- || container.getSecondaryContainer().isEmpty()) {
- // We are in an intermediate state because either the split container is about
- // to be removed or the primary or secondary container are about to receive an
- // activity.
- return null;
- }
- final ActivityStack primaryContainer = container.getPrimaryContainer()
- .toActivityStack();
- final ActivityStack secondaryContainer = container.getSecondaryContainer()
- .toActivityStack();
- final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer,
- container.getSplitAttributes());
- splitStates.add(splitState);
- }
+ mTaskContainers.valueAt(i).getSplitStates(splitStates);
}
return splitStates;
}
/**
- * Checks if all activities that are registered with the containers have already appeared in
- * the client.
+ * Whether we can now report the split states to the client.
*/
- private boolean allActivitiesCreated() {
+ @GuardedBy("mLock")
+ private boolean readyToReportToClient() {
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
- for (TaskFragmentContainer container : containers) {
- if (!container.taskInfoActivityCountMatchesCreated()) {
- return false;
- }
+ if (mTaskContainers.valueAt(i).isInIntermediateState()) {
+ // If any Task is in an intermediate state, wait for the server update.
+ return false;
}
}
return true;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 00943f2d..231da05 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -221,6 +221,24 @@
return mContainers.indexOf(child);
}
+ /** Whether the Task is in an intermediate state waiting for the server update.*/
+ boolean isInIntermediateState() {
+ for (TaskFragmentContainer container : mContainers) {
+ if (container.isInIntermediateState()) {
+ // We are in an intermediate state to wait for server update on this TaskFragment.
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */
+ void getSplitStates(@NonNull List<SplitInfo> outSplitStates) {
+ for (SplitContainer container : mSplitContainers) {
+ outSplitStates.add(container.toSplitInfo());
+ }
+ }
+
/**
* A wrapper class which contains the display ID and {@link Configuration} of a
* {@link TaskContainer}
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 18712ae..71b8840 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -166,16 +166,34 @@
return allActivities;
}
- /**
- * Checks if the count of activities from the same process in task fragment info corresponds to
- * the ones created and available on the client side.
- */
- boolean taskInfoActivityCountMatchesCreated() {
+ /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/
+ boolean isInIntermediateState() {
if (mInfo == null) {
- return false;
+ // Haven't received onTaskFragmentAppeared event.
+ return true;
}
- return mPendingAppearedActivities.isEmpty()
- && mInfo.getActivities().size() == collectNonFinishingActivities().size();
+ if (mInfo.isEmpty()) {
+ // Empty TaskFragment will be removed or will have activity launched into it soon.
+ return true;
+ }
+ if (!mPendingAppearedActivities.isEmpty()) {
+ // Reparented activity hasn't appeared.
+ return true;
+ }
+ // Check if there is any reported activity that is no longer alive.
+ for (IBinder token : mInfo.getActivities()) {
+ final Activity activity = mController.getActivity(token);
+ if (activity == null && !mTaskContainer.isVisible()) {
+ // Activity can be null if the activity is not attached to process yet. That can
+ // happen when the activity is started in background.
+ continue;
+ }
+ if (activity == null || activity.isFinishing()) {
+ // One of the reported activity is no longer alive, wait for the server update.
+ return true;
+ }
+ }
+ return false;
}
@NonNull
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 a403031..87d0278 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
@@ -102,6 +102,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.function.Consumer;
/**
* Test class for {@link SplitController}.
@@ -132,6 +133,8 @@
private SplitController mSplitController;
private SplitPresenter mSplitPresenter;
+ private Consumer<List<SplitInfo>> mEmbeddingCallback;
+ private List<SplitInfo> mSplitInfos;
private TransactionManager mTransactionManager;
@Before
@@ -141,9 +144,16 @@
.getCurrentWindowLayoutInfo(anyInt(), any());
mSplitController = new SplitController(mWindowLayoutComponent);
mSplitPresenter = mSplitController.mPresenter;
+ mSplitInfos = new ArrayList<>();
+ mEmbeddingCallback = splitInfos -> {
+ mSplitInfos.clear();
+ mSplitInfos.addAll(splitInfos);
+ };
+ mSplitController.setSplitInfoCallback(mEmbeddingCallback);
mTransactionManager = mSplitController.mTransactionManager;
spyOn(mSplitController);
spyOn(mSplitPresenter);
+ spyOn(mEmbeddingCallback);
spyOn(mTransactionManager);
doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean());
final Configuration activityConfig = new Configuration();
@@ -329,6 +339,30 @@
}
@Test
+ public void testUpdateContainer_skipIfTaskIsInvisible() {
+ final Activity r0 = createMockActivity();
+ final Activity r1 = createMockActivity();
+ addSplitTaskFragments(r0, r1);
+ final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+ final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0);
+ spyOn(taskContainer);
+
+ // No update when the Task is invisible.
+ clearInvocations(mSplitPresenter);
+ doReturn(false).when(taskContainer).isVisible();
+ mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+
+ verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+
+ // Update the split when the Task is visible.
+ doReturn(true).when(taskContainer).isVisible();
+ mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+
+ verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
+ taskFragmentContainer, mTransaction);
+ }
+
+ @Test
public void testOnStartActivityResultError() {
final Intent intent = new Intent();
final TaskContainer taskContainer = createTestTaskContainer();
@@ -1162,14 +1196,69 @@
new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
}
+ @Test
+ public void testSplitInfoCallback_reportSplit() {
+ final Activity r0 = createMockActivity();
+ final Activity r1 = createMockActivity();
+ addSplitTaskFragments(r0, r1);
+
+ mSplitController.updateCallbackIfNecessary();
+ assertEquals(1, mSplitInfos.size());
+ final SplitInfo splitInfo = mSplitInfos.get(0);
+ assertEquals(1, splitInfo.getPrimaryActivityStack().getActivities().size());
+ assertEquals(1, splitInfo.getSecondaryActivityStack().getActivities().size());
+ assertEquals(r0, splitInfo.getPrimaryActivityStack().getActivities().get(0));
+ assertEquals(r1, splitInfo.getSecondaryActivityStack().getActivities().get(0));
+ }
+
+ @Test
+ public void testSplitInfoCallback_reportSplitInMultipleTasks() {
+ final int taskId0 = 1;
+ final int taskId1 = 2;
+ final Activity r0 = createMockActivity(taskId0);
+ final Activity r1 = createMockActivity(taskId0);
+ final Activity r2 = createMockActivity(taskId1);
+ final Activity r3 = createMockActivity(taskId1);
+ addSplitTaskFragments(r0, r1);
+ addSplitTaskFragments(r2, r3);
+
+ mSplitController.updateCallbackIfNecessary();
+ assertEquals(2, mSplitInfos.size());
+ }
+
+ @Test
+ public void testSplitInfoCallback_doNotReportIfInIntermediateState() {
+ final Activity r0 = createMockActivity();
+ final Activity r1 = createMockActivity();
+ addSplitTaskFragments(r0, r1);
+ final TaskFragmentContainer tf0 = mSplitController.getContainerWithActivity(r0);
+ final TaskFragmentContainer tf1 = mSplitController.getContainerWithActivity(r1);
+ spyOn(tf0);
+ spyOn(tf1);
+
+ // Do not report if activity has not appeared in the TaskFragmentContainer in split.
+ doReturn(true).when(tf0).isInIntermediateState();
+ mSplitController.updateCallbackIfNecessary();
+ verify(mEmbeddingCallback, never()).accept(any());
+
+ doReturn(false).when(tf0).isInIntermediateState();
+ mSplitController.updateCallbackIfNecessary();
+ verify(mEmbeddingCallback).accept(any());
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
+ return createMockActivity(TASK_ID);
+ }
+
+ /** Creates a mock activity in the organizer process. */
+ private Activity createMockActivity(int taskId) {
final Activity activity = mock(Activity.class);
doReturn(mActivityResources).when(activity).getResources();
final IBinder activityToken = new Binder();
doReturn(activityToken).when(activity).getActivityToken();
doReturn(activity).when(mSplitController).getActivity(activityToken);
- doReturn(TASK_ID).when(activity).getTaskId();
+ doReturn(taskId).when(activity).getTaskId();
doReturn(new ActivityInfo()).when(activity).getActivityInfo();
doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
return activity;
@@ -1177,7 +1266,8 @@
/** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
- final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID);
+ final TaskFragmentContainer container = mSplitController.newContainer(activity,
+ activity.getTaskId());
setupTaskFragmentInfo(container, activity);
return container;
}
@@ -1268,7 +1358,7 @@
// We need to set those in case we are not respecting clear top.
// TODO(b/231845476) we should always respect clearTop.
- final int windowingMode = mSplitController.getTaskContainer(TASK_ID)
+ final int windowingMode = mSplitController.getTaskContainer(primaryContainer.getTaskId())
.getWindowingModeForSplitTaskFragment(TASK_BOUNDS);
primaryContainer.setLastRequestedWindowingMode(windowingMode);
secondaryContainer.setLastRequestedWindowingMode(windowingMode);
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 35415d8..d43c471 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
@@ -334,6 +334,70 @@
assertFalse(container.hasActivity(mActivity.getActivityToken()));
}
+ @Test
+ public void testIsInIntermediateState() {
+ // True if no info set.
+ final TaskContainer taskContainer = createTestTaskContainer();
+ final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+ mIntent, taskContainer, mController);
+ spyOn(taskContainer);
+ doReturn(true).when(taskContainer).isVisible();
+
+ assertTrue(container.isInIntermediateState());
+ assertTrue(taskContainer.isInIntermediateState());
+
+ // True if empty info set.
+ final List<IBinder> activities = new ArrayList<>();
+ doReturn(activities).when(mInfo).getActivities();
+ doReturn(true).when(mInfo).isEmpty();
+ container.setInfo(mTransaction, mInfo);
+
+ assertTrue(container.isInIntermediateState());
+ assertTrue(taskContainer.isInIntermediateState());
+
+ // False if info is not empty.
+ doReturn(false).when(mInfo).isEmpty();
+ container.setInfo(mTransaction, mInfo);
+
+ assertFalse(container.isInIntermediateState());
+ assertFalse(taskContainer.isInIntermediateState());
+
+ // True if there is pending appeared activity.
+ container.addPendingAppearedActivity(mActivity);
+
+ assertTrue(container.isInIntermediateState());
+ assertTrue(taskContainer.isInIntermediateState());
+
+ // True if the activity is finishing.
+ activities.add(mActivity.getActivityToken());
+ doReturn(true).when(mActivity).isFinishing();
+ container.setInfo(mTransaction, mInfo);
+
+ assertTrue(container.isInIntermediateState());
+ assertTrue(taskContainer.isInIntermediateState());
+
+ // False if the activity is not finishing.
+ doReturn(false).when(mActivity).isFinishing();
+ container.setInfo(mTransaction, mInfo);
+
+ assertFalse(container.isInIntermediateState());
+ assertFalse(taskContainer.isInIntermediateState());
+
+ // True if there is a token that can't find associated activity.
+ activities.clear();
+ activities.add(new Binder());
+ container.setInfo(mTransaction, mInfo);
+
+ assertTrue(container.isInIntermediateState());
+ assertTrue(taskContainer.isInIntermediateState());
+
+ // False if there is a token that can't find associated activity when the Task is invisible.
+ doReturn(false).when(taskContainer).isVisible();
+
+ assertFalse(container.isInIntermediateState());
+ assertFalse(taskContainer.isInIntermediateState());
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
final Activity activity = mock(Activity.class);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index 44a467f..cbd544c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -18,9 +18,21 @@
import com.android.wm.shell.common.annotations.ExternalThread;
+import java.util.concurrent.Executor;
+
/**
* Interface to interact with desktop mode feature in shell.
*/
@ExternalThread
public interface DesktopMode {
+
+ /**
+ * Adds a listener to find out about changes in the visibility of freeform tasks.
+ *
+ * @param listener the listener to add.
+ * @param callbackExecutor the executor to call the listener on.
+ */
+ void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+ Executor callbackExecutor);
+
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index b96facf..34ff6d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -20,6 +20,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
@@ -60,6 +61,7 @@
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.concurrent.Executor;
/**
* Handles windowing changes when desktop mode system setting changes
@@ -132,6 +134,17 @@
return new IDesktopModeImpl(this);
}
+ /**
+ * Adds a listener to find out about changes in the visibility of freeform tasks.
+ *
+ * @param listener the listener to add.
+ * @param callbackExecutor the executor to call the listener on.
+ */
+ public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+ Executor callbackExecutor) {
+ mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor);
+ }
+
@VisibleForTesting
void updateDesktopModeActive(boolean active) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
@@ -181,7 +194,18 @@
/**
* Show apps on desktop
*/
- WindowContainerTransaction showDesktopApps() {
+ void showDesktopApps() {
+ WindowContainerTransaction wct = bringDesktopAppsToFront();
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */);
+ } else {
+ mShellTaskOrganizer.applyTransaction(wct);
+ }
+ }
+
+ @NonNull
+ private WindowContainerTransaction bringDesktopAppsToFront() {
ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
@@ -197,11 +221,6 @@
for (RunningTaskInfo task : taskInfos) {
wct.reorder(task.token, true);
}
-
- if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
- mShellTaskOrganizer.applyTransaction(wct);
- }
-
return wct;
}
@@ -237,17 +256,29 @@
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
-
- // Only do anything if we are in desktop mode and opening a task/app
- if (!DesktopModeStatus.isActive(mContext) || request.getType() != TRANSIT_OPEN) {
+ // Only do anything if we are in desktop mode and opening a task/app in freeform
+ if (!DesktopModeStatus.isActive(mContext)) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "skip shell transition request: desktop mode not active");
return null;
}
+ if (request.getType() != TRANSIT_OPEN) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "skip shell transition request: only supports TRANSIT_OPEN");
+ return null;
+ }
+ if (request.getTriggerTask() == null
+ || request.getTriggerTask().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task");
+ return null;
+ }
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request);
WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this);
if (wct == null) {
wct = new WindowContainerTransaction();
}
- wct.merge(showDesktopApps(), true /* transfer */);
+ wct.merge(bringDesktopAppsToFront(), true /* transfer */);
wct.reorder(request.getTriggerTask().token, true /* onTop */);
return wct;
@@ -293,7 +324,14 @@
*/
@ExternalThread
private final class DesktopModeImpl implements DesktopMode {
- // Do nothing
+
+ @Override
+ public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+ Executor callbackExecutor) {
+ mMainExecutor.execute(() -> {
+ DesktopModeController.this.addListener(listener, callbackExecutor);
+ });
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 988601c..c91d54a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -16,7 +16,9 @@
package com.android.wm.shell.desktopmode
+import android.util.ArrayMap
import android.util.ArraySet
+import java.util.concurrent.Executor
/**
* Keeps track of task data related to desktop mode.
@@ -30,20 +32,39 @@
* Task gets removed from this list when it vanishes. Or when desktop mode is turned off.
*/
private val activeTasks = ArraySet<Int>()
- private val listeners = ArraySet<Listener>()
+ private val visibleTasks = ArraySet<Int>()
+ private val activeTasksListeners = ArraySet<ActiveTasksListener>()
+ // Track visible tasks separately because a task may be part of the desktop but not visible.
+ private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
/**
- * Add a [Listener] to be notified of updates to the repository.
+ * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository.
*/
- fun addListener(listener: Listener) {
- listeners.add(listener)
+ fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) {
+ activeTasksListeners.add(activeTasksListener)
}
/**
- * Remove a previously registered [Listener]
+ * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not.
*/
- fun removeListener(listener: Listener) {
- listeners.remove(listener)
+ fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
+ visibleTasksListeners.put(visibleTasksListener, executor)
+ executor.execute(
+ Runnable { visibleTasksListener.onVisibilityChanged(visibleTasks.size > 0) })
+ }
+
+ /**
+ * Remove a previously registered [ActiveTasksListener]
+ */
+ fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
+ activeTasksListeners.remove(activeTasksListener)
+ }
+
+ /**
+ * Remove a previously registered [VisibleTasksListener]
+ */
+ fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) {
+ visibleTasksListeners.remove(visibleTasksListener)
}
/**
@@ -52,7 +73,7 @@
fun addActiveTask(taskId: Int) {
val added = activeTasks.add(taskId)
if (added) {
- listeners.onEach { it.onActiveTasksChanged() }
+ activeTasksListeners.onEach { it.onActiveTasksChanged() }
}
}
@@ -62,7 +83,7 @@
fun removeActiveTask(taskId: Int) {
val removed = activeTasks.remove(taskId)
if (removed) {
- listeners.onEach { it.onActiveTasksChanged() }
+ activeTasksListeners.onEach { it.onActiveTasksChanged() }
}
}
@@ -81,9 +102,43 @@
}
/**
- * Defines interface for classes that can listen to changes in repository state.
+ * Updates whether a freeform task with this id is visible or not and notifies listeners.
*/
- interface Listener {
- fun onActiveTasksChanged()
+ fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) {
+ val prevCount: Int = visibleTasks.size
+ if (visible) {
+ visibleTasks.add(taskId)
+ } else {
+ visibleTasks.remove(taskId)
+ }
+ if (prevCount == 0 && visibleTasks.size == 1 ||
+ prevCount > 0 && visibleTasks.size == 0) {
+ for ((listener, executor) in visibleTasksListeners) {
+ executor.execute(
+ Runnable { listener.onVisibilityChanged(visibleTasks.size > 0) })
+ }
+ }
+ }
+
+ /**
+ * Defines interface for classes that can listen to changes for active tasks in desktop mode.
+ */
+ interface ActiveTasksListener {
+ /**
+ * Called when the active tasks change in desktop mode.
+ */
+ @JvmDefault
+ fun onActiveTasksChanged() {}
+ }
+
+ /**
+ * Defines interface for classes that can listen to changes for visible tasks in desktop mode.
+ */
+ interface VisibleTasksListener {
+ /**
+ * Called when the desktop starts or stops showing freeform tasks.
+ */
+ @JvmDefault
+ fun onVisibilityChanged(hasVisibleFreeformTasks: Boolean) {}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index f82a346..eaa7158 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -90,6 +90,8 @@
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Adding active freeform task: #%d", taskInfo.taskId);
mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
+ mDesktopModeTaskRepository.ifPresent(
+ it -> it.updateVisibleFreeformTasks(taskInfo.taskId, true));
}
}
@@ -103,6 +105,8 @@
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Removing active freeform task: #%d", taskInfo.taskId);
mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId));
+ mDesktopModeTaskRepository.ifPresent(
+ it -> it.updateVisibleFreeformTasks(taskInfo.taskId, false));
}
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -124,6 +128,8 @@
"Adding active freeform task: #%d", taskInfo.taskId);
mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
}
+ mDesktopModeTaskRepository.ifPresent(
+ it -> it.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible));
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 08f3db6..f9172ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -68,7 +68,7 @@
* Manages the recent task list from the system, caching it as necessary.
*/
public class RecentTasksController implements TaskStackListenerCallback,
- RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.Listener {
+ RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener {
private static final String TAG = RecentTasksController.class.getSimpleName();
private final Context mContext;
@@ -147,7 +147,7 @@
this::createExternalInterface, this);
mShellCommandHandler.addDumpCallback(this::dump, this);
mTaskStackListener.addListener(this);
- mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this));
+ mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
}
/**
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index c850a3b..79b520c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -20,6 +20,8 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
@@ -35,10 +37,12 @@
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.testing.AndroidTestingRunner;
import android.window.DisplayAreaInfo;
+import android.window.TransitionRequestInfo;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransaction.Change;
@@ -243,6 +247,44 @@
assertThat(op2.getContainer()).isEqualTo(token2.binder());
}
+ @Test
+ public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() {
+ when(DesktopModeStatus.isActive(any())).thenReturn(false);
+ WindowContainerTransaction wct = mController.handleRequest(
+ new Binder(),
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ assertThat(wct).isNull();
+ }
+
+ @Test
+ public void testHandleTransitionRequest_notTransitOpen_returnsNull() {
+ WindowContainerTransaction wct = mController.handleRequest(
+ new Binder(),
+ new TransitionRequestInfo(TRANSIT_TO_FRONT, null /* trigger */, null /* remote */));
+ assertThat(wct).isNull();
+ }
+
+ @Test
+ public void testHandleTransitionRequest_notFreeform_returnsNull() {
+ ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo();
+ trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ WindowContainerTransaction wct = mController.handleRequest(
+ new Binder(),
+ new TransitionRequestInfo(TRANSIT_TO_FRONT, trigger, null /* remote */));
+ assertThat(wct).isNull();
+ }
+
+ @Test
+ public void testHandleTransitionRequest_returnsWct() {
+ ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo();
+ trigger.token = new MockToken().mToken;
+ trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ WindowContainerTransaction wct = mController.handleRequest(
+ mock(IBinder.class),
+ new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */));
+ assertThat(wct).isNotNull();
+ }
+
private static class MockToken {
private final WindowContainerToken mToken;
private final IBinder mBinder;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 9b28d11..aaa5c8a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -19,6 +19,7 @@
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -38,7 +39,7 @@
@Test
fun addActiveTask_listenerNotifiedAndTaskIsActive() {
val listener = TestListener()
- repo.addListener(listener)
+ repo.addActiveTaskListener(listener)
repo.addActiveTask(1)
assertThat(listener.activeTaskChangedCalls).isEqualTo(1)
@@ -48,7 +49,7 @@
@Test
fun addActiveTask_sameTaskDoesNotNotify() {
val listener = TestListener()
- repo.addListener(listener)
+ repo.addActiveTaskListener(listener)
repo.addActiveTask(1)
repo.addActiveTask(1)
@@ -58,7 +59,7 @@
@Test
fun addActiveTask_multipleTasksAddedNotifiesForEach() {
val listener = TestListener()
- repo.addListener(listener)
+ repo.addActiveTaskListener(listener)
repo.addActiveTask(1)
repo.addActiveTask(2)
@@ -68,7 +69,7 @@
@Test
fun removeActiveTask_listenerNotifiedAndTaskNotActive() {
val listener = TestListener()
- repo.addListener(listener)
+ repo.addActiveTaskListener(listener)
repo.addActiveTask(1)
repo.removeActiveTask(1)
@@ -80,7 +81,7 @@
@Test
fun removeActiveTask_removeNotExistingTaskDoesNotNotify() {
val listener = TestListener()
- repo.addListener(listener)
+ repo.addActiveTaskListener(listener)
repo.removeActiveTask(99)
assertThat(listener.activeTaskChangedCalls).isEqualTo(0)
}
@@ -90,10 +91,69 @@
assertThat(repo.isActiveTask(99)).isFalse()
}
- class TestListener : DesktopModeTaskRepository.Listener {
+ @Test
+ fun addListener_notifiesVisibleFreeformTask() {
+ repo.updateVisibleFreeformTasks(1, true)
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+ executor.flushAll()
+
+ assertThat(listener.hasVisibleFreeformTasks).isTrue()
+ assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(1)
+ }
+
+ @Test
+ fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() {
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+ repo.updateVisibleFreeformTasks(1, true)
+ repo.updateVisibleFreeformTasks(2, true)
+ executor.flushAll()
+
+ assertThat(listener.hasVisibleFreeformTasks).isTrue()
+ // Equal to 2 because adding the listener notifies the current state
+ assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+ }
+
+ @Test
+ fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() {
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+ repo.updateVisibleFreeformTasks(1, true)
+ repo.updateVisibleFreeformTasks(2, true)
+ executor.flushAll()
+
+ assertThat(listener.hasVisibleFreeformTasks).isTrue()
+ repo.updateVisibleFreeformTasks(1, false)
+ executor.flushAll()
+
+ // Equal to 2 because adding the listener notifies the current state
+ assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+
+ repo.updateVisibleFreeformTasks(2, false)
+ executor.flushAll()
+
+ assertThat(listener.hasVisibleFreeformTasks).isFalse()
+ assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3)
+ }
+
+ class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
var activeTaskChangedCalls = 0
override fun onActiveTasksChanged() {
activeTaskChangedCalls++
}
}
+
+ class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
+ var hasVisibleFreeformTasks = false
+ var visibleFreeformTaskChangedCalls = 0
+
+ override fun onVisibilityChanged(hasVisibleTasks: Boolean) {
+ hasVisibleFreeformTasks = hasVisibleTasks
+ visibleFreeformTaskChangedCalls++
+ }
+ }
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 808ea9e..6d375ac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -549,7 +549,7 @@
try {
IActivityManager am = ActivityManager.getService();
- Configuration config = am.getConfiguration();
+ final Configuration config = new Configuration();
config.setLocales(merged);
// indicate this isn't some passing default - the user wants this remembered
config.userSetLocale = true;
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index dabb43b..89f5c2c 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -18,6 +18,7 @@
import android.graphics.drawable.Drawable
import android.view.View
import com.android.systemui.plugins.annotations.ProvidesInterface
+import com.android.systemui.plugins.log.LogBuffer
import java.io.PrintWriter
import java.util.Locale
import java.util.TimeZone
@@ -70,6 +71,9 @@
/** Optional method for dumping debug information */
fun dump(pw: PrintWriter) { }
+
+ /** Optional method for debug logging */
+ fun setLogBuffer(logBuffer: LogBuffer) { }
}
/** Interface for a specific clock face version rendered by the clock */
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
index 16a1d94..647abee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
@@ -27,6 +27,7 @@
systemui:layout_constraintEnd_toEndOf="parent"
systemui:layout_constraintTop_toTopOf="parent"
android:layout_marginHorizontal="@dimen/status_view_margin_horizontal"
+ android:clipChildren="false"
android:layout_width="0dp"
android:layout_height="wrap_content">
<LinearLayout
diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml
index b1d3375..a25ab51 100644
--- a/packages/SystemUI/res-keyguard/values/config.xml
+++ b/packages/SystemUI/res-keyguard/values/config.xml
@@ -28,11 +28,6 @@
<!-- Will display the bouncer on one side of the display, and the current user icon and
user switcher on the other side -->
<bool name="config_enableBouncerUserSwitcher">false</bool>
- <!-- Whether to show the face scanning animation on devices with face auth supported.
- The face scanning animation renders in a SW layer in ScreenDecorations.
- Enabling this will also render the camera protection in the SW layer
- (instead of HW, if relevant)."=-->
- <bool name="config_enableFaceScanningAnimation">true</bool>
<!-- Time to be considered a consecutive fingerprint failure in ms -->
<integer name="fp_consecutive_failure_time_ms">3500</integer>
</resources>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index f0e49d5..92ef3f8 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -32,41 +32,8 @@
android:layout_height="match_parent"
android:layout_width="match_parent" />
- <include
- layout="@layout/keyguard_bottom_area"
- android:visibility="gone" />
-
- <ViewStub
- android:id="@+id/keyguard_user_switcher_stub"
- android:layout="@layout/keyguard_user_switcher"
- android:layout_height="match_parent"
- android:layout_width="match_parent" />
-
<include layout="@layout/status_bar_expanded_plugin_frame"/>
- <include layout="@layout/dock_info_bottom_area_overlay" />
-
- <com.android.keyguard.LockIconView
- android:id="@+id/lock_icon_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
- <!-- Background protection -->
- <ImageView
- android:id="@+id/lock_icon_bg"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/fingerprint_bg"
- android:visibility="invisible"/>
-
- <ImageView
- android:id="@+id/lock_icon"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:scaleType="centerCrop"/>
-
- </com.android.keyguard.LockIconView>
-
<com.android.systemui.shade.NotificationsQuickSettingsContainer
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -145,6 +112,39 @@
/>
</com.android.systemui.shade.NotificationsQuickSettingsContainer>
+ <include
+ layout="@layout/keyguard_bottom_area"
+ android:visibility="gone" />
+
+ <ViewStub
+ android:id="@+id/keyguard_user_switcher_stub"
+ android:layout="@layout/keyguard_user_switcher"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent" />
+
+ <include layout="@layout/dock_info_bottom_area_overlay" />
+
+ <com.android.keyguard.LockIconView
+ android:id="@+id/lock_icon_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <!-- Background protection -->
+ <ImageView
+ android:id="@+id/lock_icon_bg"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/fingerprint_bg"
+ android:visibility="invisible"/>
+
+ <ImageView
+ android:id="@+id/lock_icon"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:scaleType="centerCrop"/>
+
+ </com.android.keyguard.LockIconView>
+
<FrameLayout
android:id="@+id/preview_container"
android:layout_width="match_parent"
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index f59a320..91fd6a6 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -62,7 +62,6 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- java_version: "1.8",
min_sdk_version: "current",
plugins: ["dagger2-compiler"],
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 1cf7c50..236aa66 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -33,6 +33,8 @@
import com.android.systemui.animation.GlyphCallback
import com.android.systemui.animation.Interpolators
import com.android.systemui.animation.TextAnimator
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
import com.android.systemui.shared.R
import java.io.PrintWriter
import java.util.Calendar
@@ -52,14 +54,8 @@
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-
- private var lastMeasureCall: CharSequence? = null
- private var lastDraw: CharSequence? = null
- private var lastTextUpdate: CharSequence? = null
- private var lastOnTextChanged: CharSequence? = null
- private var lastInvalidate: CharSequence? = null
- private var lastTimeZoneChange: CharSequence? = null
- private var lastAnimationCall: CharSequence? = null
+ var tag: String = "UnnamedClockView"
+ var logBuffer: LogBuffer? = null
private val time = Calendar.getInstance()
@@ -136,6 +132,7 @@
override fun onAttachedToWindow() {
super.onAttachedToWindow()
+ logBuffer?.log(tag, DEBUG, "onAttachedToWindow")
refreshFormat()
}
@@ -151,27 +148,39 @@
time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
contentDescription = DateFormat.format(descFormat, time)
val formattedText = DateFormat.format(format, time)
+ logBuffer?.log(tag, DEBUG,
+ { str1 = formattedText?.toString() },
+ { "refreshTime: new formattedText=$str1" }
+ )
// Setting text actually triggers a layout pass (because the text view is set to
// wrap_content width and TextView always relayouts for this). Avoid needless
// relayout if the text didn't actually change.
if (!TextUtils.equals(text, formattedText)) {
text = formattedText
+ logBuffer?.log(tag, DEBUG,
+ { str1 = formattedText?.toString() },
+ { "refreshTime: done setting new time text to: $str1" }
+ )
// Because the TextLayout may mutate under the hood as a result of the new text, we
// notify the TextAnimator that it may have changed and request a measure/layout. A
// crash will occur on the next invocation of setTextStyle if the layout is mutated
// without being notified TextInterpolator being notified.
if (layout != null) {
textAnimator?.updateLayout(layout)
+ logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout")
}
requestLayout()
- lastTextUpdate = getTimestamp()
+ logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout")
}
}
fun onTimeZoneChanged(timeZone: TimeZone?) {
time.timeZone = timeZone
refreshFormat()
- lastTimeZoneChange = "${getTimestamp()} timeZone=${time.timeZone}"
+ logBuffer?.log(tag, DEBUG,
+ { str1 = timeZone?.toString() },
+ { "onTimeZoneChanged newTimeZone=$str1" }
+ )
}
@SuppressLint("DrawAllocation")
@@ -185,27 +194,24 @@
} else {
animator.updateLayout(layout)
}
- lastMeasureCall = getTimestamp()
+ logBuffer?.log(tag, DEBUG, "onMeasure")
}
override fun onDraw(canvas: Canvas) {
- lastDraw = getTimestamp()
// Use textAnimator to render text if animation is enabled.
// Otherwise default to using standard draw functions.
if (isAnimationEnabled) {
+ // intentionally doesn't call super.onDraw here or else the text will be rendered twice
textAnimator?.draw(canvas)
} else {
super.onDraw(canvas)
}
+ logBuffer?.log(tag, DEBUG, "onDraw lastDraw")
}
override fun invalidate() {
super.invalidate()
- lastInvalidate = getTimestamp()
- }
-
- private fun getTimestamp(): CharSequence {
- return "${DateFormat.format("HH:mm:ss", System.currentTimeMillis())} text=$text"
+ logBuffer?.log(tag, DEBUG, "invalidate")
}
override fun onTextChanged(
@@ -215,7 +221,10 @@
lengthAfter: Int
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
- lastOnTextChanged = "${getTimestamp()}"
+ logBuffer?.log(tag, DEBUG,
+ { str1 = text.toString() },
+ { "onTextChanged text=$str1" }
+ )
}
fun setLineSpacingScale(scale: Float) {
@@ -229,7 +238,7 @@
}
fun animateAppearOnLockscreen() {
- lastAnimationCall = "${getTimestamp()} call=animateAppearOnLockscreen"
+ logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
setTextStyle(
weight = dozingWeight,
textSize = -1f,
@@ -254,7 +263,7 @@
if (isAnimationEnabled && textAnimator == null) {
return
}
- lastAnimationCall = "${getTimestamp()} call=animateFoldAppear"
+ logBuffer?.log(tag, DEBUG, "animateFoldAppear")
setTextStyle(
weight = lockScreenWeightInternal,
textSize = -1f,
@@ -281,7 +290,7 @@
// Skip charge animation if dozing animation is already playing.
return
}
- lastAnimationCall = "${getTimestamp()} call=animateCharge"
+ logBuffer?.log(tag, DEBUG, "animateCharge")
val startAnimPhase2 = Runnable {
setTextStyle(
weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -305,7 +314,7 @@
}
fun animateDoze(isDozing: Boolean, animate: Boolean) {
- lastAnimationCall = "${getTimestamp()} call=animateDoze"
+ logBuffer?.log(tag, DEBUG, "animateDoze")
setTextStyle(
weight = if (isDozing) dozingWeight else lockScreenWeight,
textSize = -1f,
@@ -423,9 +432,12 @@
isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
else -> DOUBLE_LINE_FORMAT_12_HOUR
}
+ logBuffer?.log(tag, DEBUG,
+ { str1 = format?.toString() },
+ { "refreshFormat format=$str1" }
+ )
descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
-
refreshTime()
}
@@ -434,15 +446,8 @@
pw.println(" measuredWidth=$measuredWidth")
pw.println(" measuredHeight=$measuredHeight")
pw.println(" singleLineInternal=$isSingleLineInternal")
- pw.println(" lastTextUpdate=$lastTextUpdate")
- pw.println(" lastOnTextChanged=$lastOnTextChanged")
- pw.println(" lastInvalidate=$lastInvalidate")
- pw.println(" lastMeasureCall=$lastMeasureCall")
- pw.println(" lastDraw=$lastDraw")
- pw.println(" lastTimeZoneChange=$lastTimeZoneChange")
pw.println(" currText=$text")
pw.println(" currTimeContextDesc=$contentDescription")
- pw.println(" lastAnimationCall=$lastAnimationCall")
pw.println(" dozingWeightInternal=$dozingWeightInternal")
pw.println(" lockScreenWeightInternal=$lockScreenWeightInternal")
pw.println(" dozingColor=$dozingColor")
@@ -591,6 +596,7 @@
if (!clockView12Skel.contains("a")) {
sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
}
+
sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
sCacheKey = key
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 6fd61da..da1d233 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -27,6 +27,7 @@
import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.shared.R
import java.io.PrintWriter
import java.util.Locale
@@ -86,9 +87,17 @@
events.onTimeTick()
}
+ override fun setLogBuffer(logBuffer: LogBuffer) {
+ smallClock.view.tag = "smallClockView"
+ largeClock.view.tag = "largeClockView"
+ smallClock.view.logBuffer = logBuffer
+ largeClock.view.logBuffer = logBuffer
+ }
+
open inner class DefaultClockFaceController(
override val view: AnimatableClockView,
) : ClockFaceController {
+
// MAGENTA is a placeholder, and will be assigned correctly in initialize
private var currentColor = Color.MAGENTA
private var isRegionDark = false
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index ce337bb..fd41cb06 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -194,12 +194,8 @@
Rect homeContentInsets, Rect minimizedHomeBounds) {
final RecentsAnimationControllerCompat controllerCompat =
new RecentsAnimationControllerCompat(controller);
- final RemoteAnimationTargetCompat[] appsCompat =
- RemoteAnimationTargetCompat.wrap(apps);
- final RemoteAnimationTargetCompat[] wallpapersCompat =
- RemoteAnimationTargetCompat.wrap(wallpapers);
- animationHandler.onAnimationStart(controllerCompat, appsCompat,
- wallpapersCompat, homeContentInsets, minimizedHomeBounds);
+ animationHandler.onAnimationStart(controllerCompat, apps,
+ wallpapers, homeContentInsets, minimizedHomeBounds);
}
@Override
@@ -210,12 +206,7 @@
@Override
public void onTasksAppeared(RemoteAnimationTarget[] apps) {
- final RemoteAnimationTargetCompat[] compats =
- new RemoteAnimationTargetCompat[apps.length];
- for (int i = 0; i < apps.length; ++i) {
- compats[i] = new RemoteAnimationTargetCompat(apps[i]);
- }
- animationHandler.onTasksAppeared(compats);
+ animationHandler.onTasksAppeared(apps);
}
};
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index f2742b7..766266d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -110,6 +110,9 @@
public static final int SYSUI_STATE_IMMERSIVE_MODE = 1 << 24;
// The voice interaction session window is showing
public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25;
+ // Freeform windows are showing in desktop mode
+ public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26;
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({SYSUI_STATE_SCREEN_PINNING,
@@ -137,7 +140,8 @@
SYSUI_STATE_BACK_DISABLED,
SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
SYSUI_STATE_IMMERSIVE_MODE,
- SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING
+ SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING,
+ SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE
})
public @interface SystemUiStateFlags {}
@@ -173,6 +177,8 @@
? "bubbles_mange_menu_expanded" : "");
str.add((flags & SYSUI_STATE_IMMERSIVE_MODE) != 0 ? "immersive_mode" : "");
str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : "");
+ str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0
+ ? "freeform_active_in_desktop_mode" : "");
return str.toString();
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
index 5cca4a6..8bddf21 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
@@ -17,6 +17,7 @@
package com.android.systemui.shared.system;
import android.graphics.Rect;
+import android.view.RemoteAnimationTarget;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -27,7 +28,7 @@
* Called when the animation into Recents can start. This call is made on the binder thread.
*/
void onAnimationStart(RecentsAnimationControllerCompat controller,
- RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers,
+ RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
Rect homeContentInsets, Rect minimizedHomeBounds);
/**
@@ -39,7 +40,7 @@
* Called when the task of an activity that has been started while the recents animation
* was running becomes ready for control.
*/
- void onTasksAppeared(RemoteAnimationTargetCompat[] app);
+ void onTasksAppeared(RemoteAnimationTarget[] app);
/**
* Called to request that the current task tile be switched out for a screenshot (if not
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
index 09cf7c5..37e706a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -83,12 +83,6 @@
RemoteAnimationTarget[] wallpapers,
RemoteAnimationTarget[] nonApps,
final IRemoteAnimationFinishedCallback finishedCallback) {
- final RemoteAnimationTargetCompat[] appsCompat =
- RemoteAnimationTargetCompat.wrap(apps);
- final RemoteAnimationTargetCompat[] wallpapersCompat =
- RemoteAnimationTargetCompat.wrap(wallpapers);
- final RemoteAnimationTargetCompat[] nonAppsCompat =
- RemoteAnimationTargetCompat.wrap(nonApps);
final Runnable animationFinishedCallback = new Runnable() {
@Override
public void run() {
@@ -100,8 +94,8 @@
}
}
};
- remoteAnimationAdapter.onAnimationStart(transit, appsCompat, wallpapersCompat,
- nonAppsCompat, animationFinishedCallback);
+ remoteAnimationAdapter.onAnimationStart(transit, apps, wallpapers,
+ nonApps, animationFinishedCallback);
}
@Override
@@ -121,12 +115,12 @@
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) {
final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
- final RemoteAnimationTargetCompat[] appsCompat =
+ final RemoteAnimationTarget[] apps =
RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
- final RemoteAnimationTargetCompat[] wallpapersCompat =
+ final RemoteAnimationTarget[] wallpapers =
RemoteAnimationTargetCompat.wrapNonApps(
info, true /* wallpapers */, t, leashMap);
- final RemoteAnimationTargetCompat[] nonAppsCompat =
+ final RemoteAnimationTarget[] nonApps =
RemoteAnimationTargetCompat.wrapNonApps(
info, false /* wallpapers */, t, leashMap);
@@ -189,9 +183,9 @@
}
}
// Make wallpaper visible immediately since launcher apparently won't do this.
- for (int i = wallpapersCompat.length - 1; i >= 0; --i) {
- t.show(wallpapersCompat[i].leash);
- t.setAlpha(wallpapersCompat[i].leash, 1.f);
+ for (int i = wallpapers.length - 1; i >= 0; --i) {
+ t.show(wallpapers[i].leash);
+ t.setAlpha(wallpapers[i].leash, 1.f);
}
} else {
if (launcherTask != null) {
@@ -237,7 +231,7 @@
}
// TODO(bc-unlcok): Pass correct transit type.
remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE,
- appsCompat, wallpapersCompat, nonAppsCompat, () -> {
+ apps, wallpapers, nonApps, () -> {
synchronized (mFinishRunnables) {
if (mFinishRunnables.remove(token) == null) return;
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
index 0076292..5809c81 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
@@ -16,11 +16,12 @@
package com.android.systemui.shared.system;
+import android.view.RemoteAnimationTarget;
import android.view.WindowManager;
public interface RemoteAnimationRunnerCompat {
void onAnimationStart(@WindowManager.TransitionOldType int transit,
- RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers,
- RemoteAnimationTargetCompat[] nonApps, Runnable finishedCallback);
+ RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps, Runnable finishedCallback);
void onAnimationCancelled();
}
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index 2d6bef5..8d1768c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -11,12 +11,15 @@
* 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
+ * limitations under the License.
*/
package com.android.systemui.shared.system;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -29,88 +32,28 @@
import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
+import android.app.TaskInfo;
import android.app.WindowConfiguration;
-import android.graphics.Point;
import android.graphics.Rect;
import android.util.ArrayMap;
-import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
+import android.window.TransitionInfo.Change;
import java.util.ArrayList;
+import java.util.function.BiPredicate;
/**
- * @see RemoteAnimationTarget
+ * Some utility methods for creating {@link RemoteAnimationTarget} instances.
*/
public class RemoteAnimationTargetCompat {
- public static final int MODE_OPENING = RemoteAnimationTarget.MODE_OPENING;
- public static final int MODE_CLOSING = RemoteAnimationTarget.MODE_CLOSING;
- public static final int MODE_CHANGING = RemoteAnimationTarget.MODE_CHANGING;
- public final int mode;
-
- public static final int ACTIVITY_TYPE_UNDEFINED = WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
- public static final int ACTIVITY_TYPE_STANDARD = WindowConfiguration.ACTIVITY_TYPE_STANDARD;
- public static final int ACTIVITY_TYPE_HOME = WindowConfiguration.ACTIVITY_TYPE_HOME;
- public static final int ACTIVITY_TYPE_RECENTS = WindowConfiguration.ACTIVITY_TYPE_RECENTS;
- public static final int ACTIVITY_TYPE_ASSISTANT = WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
- public final int activityType;
-
- public final int taskId;
- public final SurfaceControl leash;
- public final boolean isTranslucent;
- public final Rect clipRect;
- public final int prefixOrderIndex;
- public final Point position;
- public final Rect localBounds;
- public final Rect sourceContainerBounds;
- public final Rect screenSpaceBounds;
- public final Rect startScreenSpaceBounds;
- public final boolean isNotInRecents;
- public final Rect contentInsets;
- public final ActivityManager.RunningTaskInfo taskInfo;
- public final boolean allowEnterPip;
- public final int rotationChange;
- public final int windowType;
- public final WindowConfiguration windowConfiguration;
-
- private final SurfaceControl mStartLeash;
-
- // Fields used only to unwrap into RemoteAnimationTarget
- private final Rect startBounds;
-
- public final boolean willShowImeOnTarget;
-
- public RemoteAnimationTargetCompat(RemoteAnimationTarget app) {
- taskId = app.taskId;
- mode = app.mode;
- leash = app.leash;
- isTranslucent = app.isTranslucent;
- clipRect = app.clipRect;
- position = app.position;
- localBounds = app.localBounds;
- sourceContainerBounds = app.sourceContainerBounds;
- screenSpaceBounds = app.screenSpaceBounds;
- startScreenSpaceBounds = screenSpaceBounds;
- prefixOrderIndex = app.prefixOrderIndex;
- isNotInRecents = app.isNotInRecents;
- contentInsets = app.contentInsets;
- activityType = app.windowConfiguration.getActivityType();
- taskInfo = app.taskInfo;
- allowEnterPip = app.allowEnterPip;
- rotationChange = app.rotationChange;
-
- mStartLeash = app.startLeash;
- windowType = app.windowType;
- windowConfiguration = app.windowConfiguration;
- startBounds = app.startBounds;
- willShowImeOnTarget = app.willShowImeOnTarget;
- }
-
private static int newModeToLegacyMode(int newMode) {
switch (newMode) {
case WindowManager.TRANSIT_OPEN:
@@ -120,21 +63,10 @@
case WindowManager.TRANSIT_TO_BACK:
return MODE_CLOSING;
default:
- return 2; // MODE_CHANGING
+ return MODE_CHANGING;
}
}
- public RemoteAnimationTarget unwrap() {
- final RemoteAnimationTarget target = new RemoteAnimationTarget(
- taskId, mode, leash, isTranslucent, clipRect, contentInsets,
- prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration,
- isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType
- );
- target.setWillShowImeOnTarget(willShowImeOnTarget);
- target.setRotationChange(rotationChange);
- return target;
- }
-
/**
* Almost a copy of Transitions#setupStartState.
* TODO: remove when there is proper cross-process transaction sync.
@@ -206,54 +138,61 @@
return leashSurface;
}
- public RemoteAnimationTargetCompat(TransitionInfo.Change change, int order,
- TransitionInfo info, SurfaceControl.Transaction t) {
- mode = newModeToLegacyMode(change.getMode());
+ /**
+ * Creates a new RemoteAnimationTarget from the provided change info
+ */
+ public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+ TransitionInfo info, SurfaceControl.Transaction t,
+ @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+ int taskId;
+ boolean isNotInRecents;
+ ActivityManager.RunningTaskInfo taskInfo;
+ WindowConfiguration windowConfiguration;
+
taskInfo = change.getTaskInfo();
if (taskInfo != null) {
taskId = taskInfo.taskId;
isNotInRecents = !taskInfo.isRunning;
- activityType = taskInfo.getActivityType();
windowConfiguration = taskInfo.configuration.windowConfiguration;
} else {
taskId = INVALID_TASK_ID;
isNotInRecents = true;
- activityType = ACTIVITY_TYPE_UNDEFINED;
windowConfiguration = new WindowConfiguration();
}
- // TODO: once we can properly sync transactions across process, then get rid of this leash.
- leash = createLeash(info, change, order, t);
-
- isTranslucent = (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0;
- clipRect = null;
- position = null;
- localBounds = new Rect(change.getEndAbsBounds());
+ Rect localBounds = new Rect(change.getEndAbsBounds());
localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
- sourceContainerBounds = null;
- screenSpaceBounds = new Rect(change.getEndAbsBounds());
- startScreenSpaceBounds = new Rect(change.getStartAbsBounds());
- prefixOrderIndex = order;
- // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used.
- contentInsets = new Rect(0, 0, 0, 0);
- allowEnterPip = change.getAllowEnterPip();
- mStartLeash = null;
- rotationChange = change.getEndRotation() - change.getStartRotation();
- windowType = (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
- ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE;
-
- startBounds = change.getStartAbsBounds();
- willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0;
- }
-
- public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) {
- final int length = apps != null ? apps.length : 0;
- final RemoteAnimationTargetCompat[] appsCompat = new RemoteAnimationTargetCompat[length];
- for (int i = 0; i < length; i++) {
- appsCompat[i] = new RemoteAnimationTargetCompat(apps[i]);
+ RemoteAnimationTarget target = new RemoteAnimationTarget(
+ taskId,
+ newModeToLegacyMode(change.getMode()),
+ // TODO: once we can properly sync transactions across process,
+ // then get rid of this leash.
+ createLeash(info, change, order, t),
+ (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
+ null,
+ // TODO(shell-transitions): we need to send content insets? evaluate how its used.
+ new Rect(0, 0, 0, 0),
+ order,
+ null,
+ localBounds,
+ new Rect(change.getEndAbsBounds()),
+ windowConfiguration,
+ isNotInRecents,
+ null,
+ new Rect(change.getStartAbsBounds()),
+ taskInfo,
+ change.getAllowEnterPip(),
+ (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
+ ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE
+ );
+ target.setWillShowImeOnTarget(
+ (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
+ target.setRotationChange(change.getEndRotation() - change.getStartRotation());
+ if (leashMap != null) {
+ leashMap.put(change.getLeash(), target.leash);
}
- return appsCompat;
+ return target;
}
/**
@@ -262,35 +201,20 @@
* @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
* populated by this function. If null, it is ignored.
*/
- public static RemoteAnimationTargetCompat[] wrapApps(TransitionInfo info,
+ public static RemoteAnimationTarget[] wrapApps(TransitionInfo info,
SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
- final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
- final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>();
- for (int i = 0; i < info.getChanges().size(); i++) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getTaskInfo() == null) continue;
-
- final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ SparseBooleanArray childTaskTargets = new SparseBooleanArray();
+ return wrap(info, t, leashMap, (change, taskInfo) -> {
// Children always come before parent since changes are in top-to-bottom z-order.
- if (taskInfo != null) {
- if (childTaskTargets.contains(taskInfo.taskId)) {
- // has children, so not a leaf. Skip.
- continue;
- }
- if (taskInfo.hasParentTask()) {
- childTaskTargets.put(taskInfo.parentTaskId, change);
- }
+ if ((taskInfo == null) || childTaskTargets.get(taskInfo.taskId)) {
+ // has children, so not a leaf. Skip.
+ return false;
}
-
- final RemoteAnimationTargetCompat targetCompat =
- new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
- if (leashMap != null) {
- leashMap.put(change.getLeash(), targetCompat.leash);
+ if (taskInfo.hasParentTask()) {
+ childTaskTargets.put(taskInfo.parentTaskId, true);
}
- out.add(targetCompat);
- }
-
- return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+ return true;
+ });
}
/**
@@ -301,38 +225,22 @@
* @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
* populated by this function. If null, it is ignored.
*/
- public static RemoteAnimationTargetCompat[] wrapNonApps(TransitionInfo info, boolean wallpapers,
+ public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers,
SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
- final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
-
- for (int i = 0; i < info.getChanges().size(); i++) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getTaskInfo() != null) continue;
-
- final boolean changeIsWallpaper =
- (change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0;
- if (wallpapers != changeIsWallpaper) continue;
-
- final RemoteAnimationTargetCompat targetCompat =
- new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
- if (leashMap != null) {
- leashMap.put(change.getLeash(), targetCompat.leash);
- }
- out.add(targetCompat);
- }
-
- return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+ return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null)
+ && wallpapers == ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0));
}
- /**
- * @see SurfaceControl#release()
- */
- public void release() {
- if (leash != null) {
- leash.release();
+ private static RemoteAnimationTarget[] wrap(TransitionInfo info,
+ SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
+ BiPredicate<Change, TaskInfo> filter) {
+ final ArrayList<RemoteAnimationTarget> out = new ArrayList<>();
+ for (int i = 0; i < info.getChanges().size(); i++) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (filter.test(change, change.getTaskInfo())) {
+ out.add(newTarget(change, info.getChanges().size() - i, info, t, leashMap));
+ }
}
- if (mStartLeash != null) {
- mStartLeash.release();
- }
+ return out.toArray(new RemoteAnimationTarget[out.size()]);
}
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index f679225..d6655a7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -17,7 +17,9 @@
package com.android.systemui.shared.system;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
@@ -27,8 +29,7 @@
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionFilter.CONTAINER_ORDER_TOP;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.newTarget;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -45,6 +46,7 @@
import android.util.Log;
import android.util.SparseArray;
import android.view.IRecentsAnimationController;
+import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
@@ -127,9 +129,9 @@
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishedCallback) {
final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
- final RemoteAnimationTargetCompat[] apps =
+ final RemoteAnimationTarget[] apps =
RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
- final RemoteAnimationTargetCompat[] wallpapers =
+ final RemoteAnimationTarget[] wallpapers =
RemoteAnimationTargetCompat.wrapNonApps(
info, true /* wallpapers */, t, leashMap);
// TODO(b/177438007): Move this set-up logic into launcher's animation impl.
@@ -230,7 +232,7 @@
private PictureInPictureSurfaceTransaction mPipTransaction = null;
private IBinder mTransition = null;
private boolean mKeyguardLocked = false;
- private RemoteAnimationTargetCompat[] mAppearedTargets;
+ private RemoteAnimationTarget[] mAppearedTargets;
private boolean mWillFinishToHome = false;
void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info,
@@ -325,18 +327,15 @@
final int layer = mInfo.getChanges().size() * 3;
mOpeningLeashes = new ArrayList<>();
mOpeningHome = cancelRecents;
- final RemoteAnimationTargetCompat[] targets =
- new RemoteAnimationTargetCompat[openingTasks.size()];
+ final RemoteAnimationTarget[] targets =
+ new RemoteAnimationTarget[openingTasks.size()];
for (int i = 0; i < openingTasks.size(); ++i) {
final TransitionInfo.Change change = openingTasks.valueAt(i);
mOpeningLeashes.add(change.getLeash());
// We are receiving new opening tasks, so convert to onTasksAppeared.
- final RemoteAnimationTargetCompat target = new RemoteAnimationTargetCompat(
- change, layer, info, t);
- mLeashMap.put(mOpeningLeashes.get(i), target.leash);
- t.reparent(target.leash, mInfo.getRootLeash());
- t.setLayer(target.leash, layer);
- targets[i] = target;
+ targets[i] = newTarget(change, layer, info, t, mLeashMap);
+ t.reparent(targets[i].leash, mInfo.getRootLeash());
+ t.setLayer(targets[i].leash, layer);
}
t.apply();
mAppearedTargets = targets;
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 910955a..40a96b0 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -30,11 +30,14 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.DOZING_MIGRATION_1
import com.android.systemui.flags.Flags.REGION_SAMPLING
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.dagger.KeyguardClockLog
import com.android.systemui.plugins.ClockController
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.shared.regionsampling.RegionSamplingInstance
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -66,12 +69,14 @@
private val context: Context,
@Main private val mainExecutor: Executor,
@Background private val bgExecutor: Executor,
+ @KeyguardClockLog private val logBuffer: LogBuffer,
private val featureFlags: FeatureFlags
) {
var clock: ClockController? = null
set(value) {
field = value
if (value != null) {
+ value.setLogBuffer(logBuffer)
value.initialize(resources, dozeAmount, 0f)
updateRegionSamplers(value)
}
@@ -217,8 +222,11 @@
disposableHandle = parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
listenForDozing(this)
- listenForDozeAmount(this)
- listenForDozeAmountTransition(this)
+ if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ listenForDozeAmountTransition(this)
+ } else {
+ listenForDozeAmount(this)
+ }
}
}
}
@@ -261,10 +269,9 @@
@VisibleForTesting
internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
return scope.launch {
- keyguardTransitionInteractor.aodToLockscreenTransition.collect {
- // Would eventually run this:
- // dozeAmount = it.value
- // clock?.animations?.doze(dozeAmount)
+ keyguardTransitionInteractor.dozeAmountTransition.collect {
+ dozeAmount = it.value
+ clock?.animations?.doze(dozeAmount)
}
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index d03ef98..8ebad6c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -127,7 +127,7 @@
if (useLargeClock) {
out = mSmallClockFrame;
in = mLargeClockFrame;
- if (indexOfChild(in) == -1) addView(in);
+ if (indexOfChild(in) == -1) addView(in, 0);
direction = -1;
statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
+ mSmartspaceTopOffset;
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index c41b752..fe7c70a 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -23,6 +23,8 @@
import static com.android.keyguard.LockIconView.ICON_UNLOCK;
import static com.android.systemui.classifier.Classifier.LOCK_ICON;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -44,6 +46,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.android.systemui.Dumpable;
@@ -53,6 +56,10 @@
import com.android.systemui.biometrics.UdfpsController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
@@ -65,6 +72,7 @@
import java.io.PrintWriter;
import java.util.Objects;
+import java.util.function.Consumer;
import javax.inject.Inject;
@@ -101,6 +109,9 @@
@NonNull private CharSequence mLockedLabel;
@NonNull private final VibratorHelper mVibrator;
@Nullable private final AuthRippleController mAuthRippleController;
+ @NonNull private final FeatureFlags mFeatureFlags;
+ @NonNull private final KeyguardTransitionInteractor mTransitionInteractor;
+ @NonNull private final KeyguardInteractor mKeyguardInteractor;
// Tracks the velocity of a touch to help filter out the touches that move too fast.
private VelocityTracker mVelocityTracker;
@@ -137,6 +148,20 @@
private boolean mDownDetected;
private final Rect mSensorTouchLocation = new Rect();
+ @VisibleForTesting
+ final Consumer<TransitionStep> mDozeTransitionCallback = (TransitionStep step) -> {
+ mInterpolatedDarkAmount = step.getValue();
+ mView.setDozeAmount(step.getValue());
+ updateBurnInOffsets();
+ };
+
+ @VisibleForTesting
+ final Consumer<Boolean> mIsDozingCallback = (Boolean isDozing) -> {
+ mIsDozing = isDozing;
+ updateBurnInOffsets();
+ updateVisibility();
+ };
+
@Inject
public LockIconViewController(
@Nullable LockIconView view,
@@ -152,7 +177,10 @@
@NonNull @Main DelayableExecutor executor,
@NonNull VibratorHelper vibrator,
@Nullable AuthRippleController authRippleController,
- @NonNull @Main Resources resources
+ @NonNull @Main Resources resources,
+ @NonNull KeyguardTransitionInteractor transitionInteractor,
+ @NonNull KeyguardInteractor keyguardInteractor,
+ @NonNull FeatureFlags featureFlags
) {
super(view);
mStatusBarStateController = statusBarStateController;
@@ -166,6 +194,9 @@
mExecutor = executor;
mVibrator = vibrator;
mAuthRippleController = authRippleController;
+ mTransitionInteractor = transitionInteractor;
+ mKeyguardInteractor = keyguardInteractor;
+ mFeatureFlags = featureFlags;
mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
@@ -182,6 +213,12 @@
@Override
protected void onInit() {
mView.setAccessibilityDelegate(mAccessibilityDelegate);
+
+ if (mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ collectFlow(mView, mTransitionInteractor.getDozeAmountTransition(),
+ mDozeTransitionCallback);
+ collectFlow(mView, mKeyguardInteractor.isDozing(), mIsDozingCallback);
+ }
}
@Override
@@ -377,14 +414,17 @@
pw.println(" mShowUnlockIcon: " + mShowUnlockIcon);
pw.println(" mShowLockIcon: " + mShowLockIcon);
pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon);
- pw.println(" mIsDozing: " + mIsDozing);
- pw.println(" mIsBouncerShowing: " + mIsBouncerShowing);
- pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
- pw.println(" mRunningFPS: " + mRunningFPS);
- pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen);
- pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState));
- pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
- pw.println(" mSensorTouchLocation: " + mSensorTouchLocation);
+ pw.println();
+ pw.println(" mIsDozing: " + mIsDozing);
+ pw.println(" isFlagEnabled(DOZING_MIGRATION_1): "
+ + mFeatureFlags.isEnabled(DOZING_MIGRATION_1));
+ pw.println(" mIsBouncerShowing: " + mIsBouncerShowing);
+ pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
+ pw.println(" mRunningFPS: " + mRunningFPS);
+ pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen);
+ pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState));
+ pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
+ pw.println(" mSensorTouchLocation: " + mSensorTouchLocation);
if (mView != null) {
mView.dump(pw, args);
@@ -425,16 +465,20 @@
new StatusBarStateController.StateListener() {
@Override
public void onDozeAmountChanged(float linear, float eased) {
- mInterpolatedDarkAmount = eased;
- mView.setDozeAmount(eased);
- updateBurnInOffsets();
+ if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ mInterpolatedDarkAmount = eased;
+ mView.setDozeAmount(eased);
+ updateBurnInOffsets();
+ }
}
@Override
public void onDozingChanged(boolean isDozing) {
- mIsDozing = isDozing;
- updateBurnInOffsets();
- updateVisibility();
+ if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ mIsDozing = isDozing;
+ updateBurnInOffsets();
+ updateVisibility();
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index e74d810..80c6c48 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -127,7 +127,7 @@
private final ScrollView mBiometricScrollView;
private final View mPanelView;
private final float mTranslationY;
- @ContainerState private int mContainerState = STATE_UNKNOWN;
+ @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
private final Set<Integer> mFailedModalities = new HashSet<Integer>();
private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
@@ -485,45 +485,18 @@
mContainerState = STATE_SHOWING;
} else {
mContainerState = STATE_ANIMATING_IN;
- // The background panel and content are different views since we need to be able to
- // animate them separately in other places.
- mPanelView.setY(mTranslationY);
- mBiometricScrollView.setY(mTranslationY);
-
+ setY(mTranslationY);
setAlpha(0f);
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
postOnAnimation(() -> {
- mPanelView.animate()
- .translationY(0)
- .setDuration(animateDuration)
- .setInterpolator(mLinearOutSlowIn)
- .setListener(getJankListener(mPanelView, SHOW, animateDuration))
- .withLayer()
- .withEndAction(this::onDialogAnimatedIn)
- .start();
- mBiometricScrollView.animate()
- .translationY(0)
- .setDuration(animateDuration)
- .setInterpolator(mLinearOutSlowIn)
- .setListener(getJankListener(mBiometricScrollView, SHOW, animateDuration))
- .withLayer()
- .start();
- if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
- mCredentialView.setY(mTranslationY);
- mCredentialView.animate()
- .translationY(0)
- .setDuration(animateDuration)
- .setInterpolator(mLinearOutSlowIn)
- .setListener(getJankListener(mCredentialView, SHOW, animateDuration))
- .withLayer()
- .start();
- }
animate()
.alpha(1f)
+ .translationY(0)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.setListener(getJankListener(this, SHOW, animateDuration))
+ .withEndAction(this::onDialogAnimatedIn)
.start();
});
}
@@ -657,11 +630,25 @@
wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
}
+ private void forceExecuteAnimatedIn() {
+ if (mContainerState == STATE_ANIMATING_IN) {
+ //clear all animators
+ if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
+ mCredentialView.animate().cancel();
+ }
+ mPanelView.animate().cancel();
+ mBiometricView.animate().cancel();
+ animate().cancel();
+ onDialogAnimatedIn();
+ }
+ }
+
@Override
public void dismissWithoutCallback(boolean animate) {
if (animate) {
animateAway(false /* sendReason */, 0 /* reason */);
} else {
+ forceExecuteAnimatedIn();
removeWindowIfAttached();
}
}
@@ -788,32 +775,9 @@
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
postOnAnimation(() -> {
- mPanelView.animate()
- .translationY(mTranslationY)
- .setDuration(animateDuration)
- .setInterpolator(mLinearOutSlowIn)
- .setListener(getJankListener(mPanelView, DISMISS, animateDuration))
- .withLayer()
- .withEndAction(endActionRunnable)
- .start();
- mBiometricScrollView.animate()
- .translationY(mTranslationY)
- .setDuration(animateDuration)
- .setInterpolator(mLinearOutSlowIn)
- .setListener(getJankListener(mBiometricScrollView, DISMISS, animateDuration))
- .withLayer()
- .start();
- if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
- mCredentialView.animate()
- .translationY(mTranslationY)
- .setDuration(animateDuration)
- .setInterpolator(mLinearOutSlowIn)
- .setListener(getJankListener(mCredentialView, DISMISS, animateDuration))
- .withLayer()
- .start();
- }
animate()
.alpha(0f)
+ .translationY(mTranslationY)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.setListener(getJankListener(this, DISMISS, animateDuration))
@@ -828,6 +792,7 @@
mWindowManager.updateViewLayout(this, lp);
})
.withLayer()
+ .withEndAction(endActionRunnable)
.start();
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 3b41a2d..c015a21 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -294,6 +294,8 @@
}
});
mUdfpsController.setAuthControllerUpdateUdfpsLocation(this::updateUdfpsLocation);
+ mUdfpsController.setUdfpsDisplayMode(new UdfpsDisplayMode(mContext, mExecution,
+ this));
mUdfpsBounds = mUdfpsProps.get(0).getLocation().getRect();
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index d17f787..b49d452 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -123,7 +123,6 @@
@NonNull private final PowerManager mPowerManager;
@NonNull private final AccessibilityManager mAccessibilityManager;
@NonNull private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
- @Nullable private final UdfpsDisplayModeProvider mUdfpsDisplayMode;
@NonNull private final ConfigurationController mConfigurationController;
@NonNull private final SystemClock mSystemClock;
@NonNull private final UnlockedScreenOffAnimationController
@@ -139,6 +138,7 @@
// TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this.
@Nullable private Runnable mAuthControllerUpdateUdfpsLocation;
@Nullable private final AlternateUdfpsTouchProvider mAlternateTouchProvider;
+ @Nullable private UdfpsDisplayMode mUdfpsDisplayMode;
// Tracks the velocity of a touch to help filter out the touches that move too fast.
@Nullable private VelocityTracker mVelocityTracker;
@@ -319,6 +319,10 @@
mAuthControllerUpdateUdfpsLocation = r;
}
+ public void setUdfpsDisplayMode(UdfpsDisplayMode udfpsDisplayMode) {
+ mUdfpsDisplayMode = udfpsDisplayMode;
+ }
+
/**
* Calculate the pointer speed given a velocity tracker and the pointer id.
* This assumes that the velocity tracker has already been passed all relevant motion events.
@@ -594,7 +598,6 @@
@NonNull VibratorHelper vibrator,
@NonNull UdfpsHapticsSimulator udfpsHapticsSimulator,
@NonNull UdfpsShell udfpsShell,
- @NonNull Optional<UdfpsDisplayModeProvider> udfpsDisplayMode,
@NonNull KeyguardStateController keyguardStateController,
@NonNull DisplayManager displayManager,
@Main Handler mainHandler,
@@ -626,7 +629,6 @@
mPowerManager = powerManager;
mAccessibilityManager = accessibilityManager;
mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
- mUdfpsDisplayMode = udfpsDisplayMode.orElse(null);
screenLifecycle.addObserver(mScreenObserver);
mScreenOn = screenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_ON;
mConfigurationController = configurationController;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt
new file mode 100644
index 0000000..e9de7cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt
@@ -0,0 +1,88 @@
+package com.android.systemui.biometrics
+
+import android.content.Context
+import android.os.RemoteException
+import android.os.Trace
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.concurrency.Execution
+import javax.inject.Inject
+
+private const val TAG = "UdfpsDisplayMode"
+
+/**
+ * UdfpsDisplayMode that encapsulates pixel-specific code, such as enabling the high-brightness mode
+ * (HBM) in a display-specific way and freezing the display's refresh rate.
+ */
+@SysUISingleton
+class UdfpsDisplayMode
+@Inject
+constructor(
+ private val context: Context,
+ private val execution: Execution,
+ private val authController: AuthController
+) : UdfpsDisplayModeProvider {
+
+ // The request is reset to null after it's processed.
+ private var currentRequest: Request? = null
+
+ override fun enable(onEnabled: Runnable?) {
+ execution.isMainThread()
+ Log.v(TAG, "enable")
+
+ if (currentRequest != null) {
+ Log.e(TAG, "enable | already requested")
+ return
+ }
+ if (authController.udfpsHbmListener == null) {
+ Log.e(TAG, "enable | mDisplayManagerCallback is null")
+ return
+ }
+
+ Trace.beginSection("UdfpsDisplayMode.enable")
+
+ // Track this request in one object.
+ val request = Request(context.displayId)
+ currentRequest = request
+
+ try {
+ // This method is a misnomer. It has nothing to do with HBM, its purpose is to set
+ // the appropriate display refresh rate.
+ authController.udfpsHbmListener!!.onHbmEnabled(request.displayId)
+ Log.v(TAG, "enable | requested optimal refresh rate for UDFPS")
+ } catch (e: RemoteException) {
+ Log.e(TAG, "enable", e)
+ }
+
+ onEnabled?.run() ?: Log.w(TAG, "enable | onEnabled is null")
+ Trace.endSection()
+ }
+
+ override fun disable(onDisabled: Runnable?) {
+ execution.isMainThread()
+ Log.v(TAG, "disable")
+
+ val request = currentRequest
+ if (request == null) {
+ Log.w(TAG, "disable | already disabled")
+ return
+ }
+
+ Trace.beginSection("UdfpsDisplayMode.disable")
+
+ try {
+ // Allow DisplayManager to unset the UDFPS refresh rate.
+ authController.udfpsHbmListener!!.onHbmDisabled(request.displayId)
+ Log.v(TAG, "disable | removed the UDFPS refresh rate request")
+ } catch (e: RemoteException) {
+ Log.e(TAG, "disable", e)
+ }
+
+ currentRequest = null
+ onDisabled?.run() ?: Log.w(TAG, "disable | onDisabled is null")
+ Trace.endSection()
+ }
+}
+
+/** Tracks a request to enable the UDFPS mode. */
+private data class Request(val displayId: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index bfb27a4..9f338d1 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -459,7 +459,7 @@
anim.start();
}
- private void hideImmediate() {
+ void hideImmediate() {
// Note this may be called multiple times if multiple dismissal events happen at the same
// time.
mTimeoutHandler.cancelTimeout();
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index c256e44..976afd4 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -34,8 +34,6 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -47,15 +45,13 @@
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@Main private val mainExecutor: Executor,
- private val featureFlags: FeatureFlags
) : DecorProviderFactory() {
private val display = context.display
private val displayInfo = DisplayInfo()
override val hasProviders: Boolean
get() {
- if (!featureFlags.isEnabled(Flags.FACE_SCANNING_ANIM) ||
- authController.faceSensorLocation == null) {
+ if (authController.faceSensorLocation == null) {
return false
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e84a564..06b7614 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -63,7 +63,8 @@
@JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true)
val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true)
val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true)
- // next id: 116
+ @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, true)
+ // next id: 117
// 200 - keyguard/lockscreen
// ** Flag retired **
@@ -80,9 +81,6 @@
@JvmField
val BOUNCER_USER_SWITCHER = ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher)
- // TODO(b/254512694): Tracking Bug
- val FACE_SCANNING_ANIM = ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation)
-
// TODO(b/254512676): Tracking Bug
@JvmField val LOCKSCREEN_CUSTOM_CLOCKS = UnreleasedFlag(207, teamfood = true)
@@ -120,6 +118,12 @@
*/
@JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212)
+ /**
+ * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
+ * will occur in stages. This is one stage of many to come.
+ */
+ @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true)
+
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = ReleasedFlag(300)
@@ -192,7 +196,7 @@
// 802 - wallpaper rendering
// TODO(b/254512923): Tracking Bug
- @JvmField val USE_CANVAS_RENDERER = UnreleasedFlag(802, teamfood = true)
+ @JvmField val USE_CANVAS_RENDERER = ReleasedFlag(802)
// 803 - screen contents translation
// TODO(b/254513187): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 59bb22786..7409aec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -24,6 +24,8 @@
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
/** Encapsulates business-logic related to the keyguard transitions. */
@SysUISingleton
@@ -34,4 +36,17 @@
) {
/** AOD->LOCKSCREEN transition information. */
val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
+
+ /** LOCKSCREEN->AOD transition information. */
+ val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+
+ /**
+ * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
+ * Lockscreen (0f).
+ */
+ val dozeAmountTransition: Flow<TransitionStep> =
+ merge(
+ aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) },
+ lockscreenToAodTransition,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
new file mode 100644
index 0000000..0645236
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardClockLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index e3311db..9adb855 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -316,6 +316,16 @@
}
/**
+ * Provides a {@link LogBuffer} for keyguard clock logs.
+ */
+ @Provides
+ @SysUISingleton
+ @KeyguardClockLog
+ public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) {
+ return factory.create("KeyguardClockLog", 500);
+ }
+
+ /**
* Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}.
*/
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 26cbcbf..1b9cdd4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -767,7 +767,9 @@
mShareChip.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
- mActionExecutor.launchIntentAsync(ActionIntentCreator.INSTANCE.createShareIntent(
+ prepareSharedTransition();
+ mActionExecutor.launchIntentAsync(
+ ActionIntentCreator.INSTANCE.createShareIntent(
imageData.uri, imageData.subject),
imageData.shareTransition.get().bundle,
imageData.owner.getIdentifier(), false);
@@ -778,6 +780,7 @@
mEditChip.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ prepareSharedTransition();
mActionExecutor.launchIntentAsync(
ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
imageData.editTransition.get().bundle,
@@ -789,6 +792,7 @@
mScreenshotPreview.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ prepareSharedTransition();
mActionExecutor.launchIntentAsync(
ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
imageData.editTransition.get().bundle,
@@ -1064,6 +1068,12 @@
}
}
+ private void prepareSharedTransition() {
+ mPendingSharedTransition = true;
+ // fade out non-preview UI
+ createScreenshotFadeDismissAnimation().start();
+ }
+
ValueAnimator createScreenshotFadeDismissAnimation() {
ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
alphaAnim.addUpdateListener(animation -> {
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index 6e9f859..d5a3954 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -20,6 +20,7 @@
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import android.app.Activity;
+import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.view.Gravity;
@@ -36,6 +37,8 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
+import java.util.List;
+
import javax.inject.Inject;
/** A dialog that provides controls for adjusting the screen brightness. */
@@ -83,6 +86,15 @@
lp.leftMargin = horizontalMargin;
lp.rightMargin = horizontalMargin;
frame.setLayoutParams(lp);
+ Rect bounds = new Rect();
+ frame.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ // Exclude this view (and its horizontal margins) from triggering gestures.
+ // This prevents back gesture from being triggered by dragging close to the
+ // edge of the slider (0% or 100%).
+ bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top);
+ v.setSystemGestureExclusionRects(List.of(bounds));
+ });
BrightnessSliderController controller = mToggleSliderFactory.create(this, frame);
controller.init();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index f961984..87ef92a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -40,6 +40,7 @@
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -110,8 +111,8 @@
setClipChildren(false);
setClipToPadding(false);
mShelfIcons.setIsStaticLayout(false);
- setBottomRoundness(1.0f, false /* animate */);
- setTopRoundness(1f, false /* animate */);
+ requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue);
+ requestTopRoundness(1f, false, SourceType.DefaultValue);
// Setting this to first in section to get the clipping to the top roundness correct. This
// value determines the way we are clipping to the top roundness of the overall shade
@@ -413,7 +414,7 @@
if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
// only if the first icon is fully in the shelf we want to clip to it!
backgroundTop = (int) (child.getTranslationY() - getTranslationY());
- firstElementRoundness = expandableRow.getCurrentTopRoundness();
+ firstElementRoundness = expandableRow.getTopRoundness();
}
}
@@ -507,28 +508,36 @@
// Round bottom corners within animation bounds
final float changeFraction = MathUtils.saturate(
(viewEnd - cornerAnimationTop) / cornerAnimationDistance);
- anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction,
- false /* animate */);
+ anv.requestBottomRoundness(
+ anv.isLastInSection() ? 1f : changeFraction,
+ /* animate = */ false,
+ SourceType.OnScroll);
} else if (viewEnd < cornerAnimationTop) {
// Fast scroll skips frames and leaves corners with unfinished rounding.
// Reset top and bottom corners outside of animation bounds.
- anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius,
- false /* animate */);
+ anv.requestBottomRoundness(
+ anv.isLastInSection() ? 1f : smallCornerRadius,
+ /* animate = */ false,
+ SourceType.OnScroll);
}
if (viewStart >= cornerAnimationTop) {
// Round top corners within animation bounds
final float changeFraction = MathUtils.saturate(
(viewStart - cornerAnimationTop) / cornerAnimationDistance);
- anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction,
- false /* animate */);
+ anv.requestTopRoundness(
+ anv.isFirstInSection() ? 1f : changeFraction,
+ false,
+ SourceType.OnScroll);
} else if (viewStart < cornerAnimationTop) {
// Fast scroll skips frames and leaves corners with unfinished rounding.
// Reset top and bottom corners outside of animation bounds.
- anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius,
- false /* animate */);
+ anv.requestTopRoundness(
+ anv.isFirstInSection() ? 1f : smallCornerRadius,
+ false,
+ SourceType.OnScroll);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 553826d..0d35fdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -70,8 +70,8 @@
val height = max(0, notification.actualHeight - notification.clipBottomAmount)
val location = notification.locationOnScreen
- val clipStartLocation = notificationListContainer.getTopClippingStartLocation()
- val roundedTopClipping = Math.max(clipStartLocation - location[1], 0)
+ val clipStartLocation = notificationListContainer.topClippingStartLocation
+ val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0)
val windowTop = location[1] + roundedTopClipping
val topCornerRadius = if (roundedTopClipping > 0) {
// Because the rounded Rect clipping is complex, we start the top rounding at
@@ -80,7 +80,7 @@
// if we'd like to have this perfect, but this is close enough.
0f
} else {
- notification.currentBackgroundRadiusTop
+ notification.topCornerRadius
}
val params = LaunchAnimationParameters(
top = windowTop,
@@ -88,7 +88,7 @@
left = location[0],
right = location[0] + notification.width,
topCornerRadius = topCornerRadius,
- bottomCornerRadius = notification.currentBackgroundRadiusBottom
+ bottomCornerRadius = notification.bottomCornerRadius
)
params.startTranslationZ = notification.translationZ
@@ -97,8 +97,8 @@
params.startClipTopAmount = notification.clipTopAmount
if (notification.isChildInGroup) {
params.startNotificationTop += notification.notificationParent.translationY
- val parentRoundedClip = Math.max(
- clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
+ val locationOnScreen = notification.notificationParent.locationOnScreen[1]
+ val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0)
params.parentStartRoundedTopClipping = parentRoundedClip
val parentClip = notification.notificationParent.clipTopAmount
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
new file mode 100644
index 0000000..ed7f648
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -0,0 +1,284 @@
+package com.android.systemui.statusbar.notification
+
+import android.util.FloatProperty
+import android.view.View
+import androidx.annotation.FloatRange
+import com.android.systemui.R
+import com.android.systemui.statusbar.notification.stack.AnimationProperties
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import kotlin.math.abs
+
+/**
+ * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f).
+ *
+ * To request a roundness value, an [SourceType] must be specified. In case more origins require
+ * different roundness, for the same property, the maximum value will always be chosen.
+ *
+ * It also returns the current radius for all corners ([updatedRadii]).
+ */
+interface Roundable {
+ /** Properties required for a Roundable */
+ val roundableState: RoundableState
+
+ /** Current top roundness */
+ @get:FloatRange(from = 0.0, to = 1.0)
+ @JvmDefault
+ val topRoundness: Float
+ get() = roundableState.topRoundness
+
+ /** Current bottom roundness */
+ @get:FloatRange(from = 0.0, to = 1.0)
+ @JvmDefault
+ val bottomRoundness: Float
+ get() = roundableState.bottomRoundness
+
+ /** Max radius in pixel */
+ @JvmDefault
+ val maxRadius: Float
+ get() = roundableState.maxRadius
+
+ /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
+ @JvmDefault
+ val topCornerRadius: Float
+ get() = topRoundness * maxRadius
+
+ /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
+ @JvmDefault
+ val bottomCornerRadius: Float
+ get() = bottomRoundness * maxRadius
+
+ /** Get and update the current radii */
+ @JvmDefault
+ val updatedRadii: FloatArray
+ get() =
+ roundableState.radiiBuffer.also { radii ->
+ updateRadii(
+ topCornerRadius = topCornerRadius,
+ bottomCornerRadius = bottomCornerRadius,
+ radii = radii,
+ )
+ }
+
+ /**
+ * Request the top roundness [value] for a specific [sourceType].
+ *
+ * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
+ * origins require different roundness, for the same property, the maximum value will always be
+ * chosen.
+ *
+ * @param value a value between 0f and 1f.
+ * @param animate true if it should animate to that value.
+ * @param sourceType the source from which the request for roundness comes.
+ * @return Whether the roundness was changed.
+ */
+ @JvmDefault
+ fun requestTopRoundness(
+ @FloatRange(from = 0.0, to = 1.0) value: Float,
+ animate: Boolean,
+ sourceType: SourceType,
+ ): Boolean {
+ val roundnessMap = roundableState.topRoundnessMap
+ val lastValue = roundnessMap.values.maxOrNull() ?: 0f
+ if (value == 0f) {
+ // we should only take the largest value, and since the smallest value is 0f, we can
+ // remove this value from the list. In the worst case, the list is empty and the
+ // default value is 0f.
+ roundnessMap.remove(sourceType)
+ } else {
+ roundnessMap[sourceType] = value
+ }
+ val newValue = roundnessMap.values.maxOrNull() ?: 0f
+
+ if (lastValue != newValue) {
+ val wasAnimating = roundableState.isTopAnimating()
+
+ // Fail safe:
+ // when we've been animating previously and we're now getting an update in the
+ // other direction, make sure to animate it too, otherwise, the localized updating
+ // may make the start larger than 1.0.
+ val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
+
+ roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate)
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Request the bottom roundness [value] for a specific [sourceType].
+ *
+ * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
+ * origins require different roundness, for the same property, the maximum value will always be
+ * chosen.
+ *
+ * @param value value between 0f and 1f.
+ * @param animate true if it should animate to that value.
+ * @param sourceType the source from which the request for roundness comes.
+ * @return Whether the roundness was changed.
+ */
+ @JvmDefault
+ fun requestBottomRoundness(
+ @FloatRange(from = 0.0, to = 1.0) value: Float,
+ animate: Boolean,
+ sourceType: SourceType,
+ ): Boolean {
+ val roundnessMap = roundableState.bottomRoundnessMap
+ val lastValue = roundnessMap.values.maxOrNull() ?: 0f
+ if (value == 0f) {
+ // we should only take the largest value, and since the smallest value is 0f, we can
+ // remove this value from the list. In the worst case, the list is empty and the
+ // default value is 0f.
+ roundnessMap.remove(sourceType)
+ } else {
+ roundnessMap[sourceType] = value
+ }
+ val newValue = roundnessMap.values.maxOrNull() ?: 0f
+
+ if (lastValue != newValue) {
+ val wasAnimating = roundableState.isBottomAnimating()
+
+ // Fail safe:
+ // when we've been animating previously and we're now getting an update in the
+ // other direction, make sure to animate it too, otherwise, the localized updating
+ // may make the start larger than 1.0.
+ val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
+
+ roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate)
+ return true
+ }
+ return false
+ }
+
+ /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
+ @JvmDefault
+ fun applyRoundness() {
+ roundableState.targetView.invalidate()
+ }
+
+ /** @return true if top or bottom roundness is not zero. */
+ @JvmDefault
+ fun hasRoundedCorner(): Boolean {
+ return topRoundness != 0f || bottomRoundness != 0f
+ }
+
+ /**
+ * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of
+ * [android.graphics.Path.addRoundRect].
+ *
+ * This method reuses the previous [radii] for performance reasons.
+ */
+ @JvmDefault
+ fun updateRadii(
+ topCornerRadius: Float,
+ bottomCornerRadius: Float,
+ radii: FloatArray,
+ ) {
+ if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}")
+
+ if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) {
+ (0..3).forEach { radii[it] = topCornerRadius }
+ (4..7).forEach { radii[it] = bottomCornerRadius }
+ }
+ }
+}
+
+/**
+ * State object for a `Roundable` class.
+ * @param targetView Will handle the [AnimatableProperty]
+ * @param roundable Target of the radius animation
+ * @param maxRadius Max corner radius in pixels
+ */
+class RoundableState(
+ internal val targetView: View,
+ roundable: Roundable,
+ internal val maxRadius: Float,
+) {
+ /** Animatable for top roundness */
+ private val topAnimatable = topAnimatable(roundable)
+
+ /** Animatable for bottom roundness */
+ private val bottomAnimatable = bottomAnimatable(roundable)
+
+ /** Current top roundness. Use [setTopRoundness] to update this value */
+ @set:FloatRange(from = 0.0, to = 1.0)
+ internal var topRoundness = 0f
+ private set
+
+ /** Current bottom roundness. Use [setBottomRoundness] to update this value */
+ @set:FloatRange(from = 0.0, to = 1.0)
+ internal var bottomRoundness = 0f
+ private set
+
+ /** Last requested top roundness associated by [SourceType] */
+ internal val topRoundnessMap = mutableMapOf<SourceType, Float>()
+
+ /** Last requested bottom roundness associated by [SourceType] */
+ internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>()
+
+ /** Last cached radii */
+ internal val radiiBuffer = FloatArray(8)
+
+ /** Is top roundness animation in progress? */
+ internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable)
+
+ /** Is bottom roundness animation in progress? */
+ internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable)
+
+ /** Set the current top roundness */
+ internal fun setTopRoundness(
+ value: Float,
+ animated: Boolean = targetView.isShown,
+ ) {
+ PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
+ }
+
+ /** Set the current bottom roundness */
+ internal fun setBottomRoundness(
+ value: Float,
+ animated: Boolean = targetView.isShown,
+ ) {
+ PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
+ }
+
+ companion object {
+ private val DURATION: AnimationProperties =
+ AnimationProperties()
+ .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong())
+
+ private fun topAnimatable(roundable: Roundable): AnimatableProperty =
+ AnimatableProperty.from(
+ object : FloatProperty<View>("topRoundness") {
+ override fun get(view: View): Float = roundable.topRoundness
+
+ override fun setValue(view: View, value: Float) {
+ roundable.roundableState.topRoundness = value
+ roundable.applyRoundness()
+ }
+ },
+ R.id.top_roundess_animator_tag,
+ R.id.top_roundess_animator_end_tag,
+ R.id.top_roundess_animator_start_tag,
+ )
+
+ private fun bottomAnimatable(roundable: Roundable): AnimatableProperty =
+ AnimatableProperty.from(
+ object : FloatProperty<View>("bottomRoundness") {
+ override fun get(view: View): Float = roundable.bottomRoundness
+
+ override fun setValue(view: View, value: Float) {
+ roundable.roundableState.bottomRoundness = value
+ roundable.applyRoundness()
+ }
+ },
+ R.id.bottom_roundess_animator_tag,
+ R.id.bottom_roundess_animator_end_tag,
+ R.id.bottom_roundess_animator_start_tag,
+ )
+ }
+}
+
+enum class SourceType {
+ DefaultValue,
+ OnDismissAnimation,
+ OnScroll,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 755e3e1..d29298a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -613,22 +613,21 @@
protected void resetAllContentAlphas() {}
@Override
- protected void applyRoundness() {
+ public void applyRoundness() {
super.applyRoundness();
- applyBackgroundRoundness(getCurrentBackgroundRadiusTop(),
- getCurrentBackgroundRadiusBottom());
+ applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius());
}
@Override
- public float getCurrentBackgroundRadiusTop() {
+ public float getTopCornerRadius() {
float fraction = getInterpolatedAppearAnimationFraction();
- return MathUtils.lerp(0, super.getCurrentBackgroundRadiusTop(), fraction);
+ return MathUtils.lerp(0, super.getTopCornerRadius(), fraction);
}
@Override
- public float getCurrentBackgroundRadiusBottom() {
+ public float getBottomCornerRadius() {
float fraction = getInterpolatedAppearAnimationFraction();
- return MathUtils.lerp(0, super.getCurrentBackgroundRadiusBottom(), fraction);
+ return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction);
}
private void applyBackgroundRoundness(float topRadius, float bottomRadius) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 087dc71..9e7717c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -93,6 +93,7 @@
import com.android.systemui.statusbar.notification.NotificationFadeAware;
import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
@@ -154,7 +155,9 @@
void onLayout();
}
- /** Listens for changes to the expansion state of this row. */
+ /**
+ * Listens for changes to the expansion state of this row.
+ */
public interface OnExpansionChangedListener {
void onExpansionChanged(boolean isExpanded);
}
@@ -183,22 +186,34 @@
private int mNotificationLaunchHeight;
private boolean mMustStayOnScreen;
- /** Does this row contain layouts that can adapt to row expansion */
+ /**
+ * Does this row contain layouts that can adapt to row expansion
+ */
private boolean mExpandable;
- /** Has the user actively changed the expansion state of this row */
+ /**
+ * Has the user actively changed the expansion state of this row
+ */
private boolean mHasUserChangedExpansion;
- /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
+ /**
+ * If {@link #mHasUserChangedExpansion}, has the user expanded this row
+ */
private boolean mUserExpanded;
- /** Whether the blocking helper is showing on this notification (even if dismissed) */
+ /**
+ * Whether the blocking helper is showing on this notification (even if dismissed)
+ */
private boolean mIsBlockingHelperShowing;
/**
* Has this notification been expanded while it was pinned
*/
private boolean mExpandedWhenPinned;
- /** Is the user touching this row */
+ /**
+ * Is the user touching this row
+ */
private boolean mUserLocked;
- /** Are we showing the "public" version */
+ /**
+ * Are we showing the "public" version
+ */
private boolean mShowingPublic;
private boolean mSensitive;
private boolean mSensitiveHiddenInGeneral;
@@ -351,11 +366,14 @@
private boolean mWasChildInGroupWhenRemoved;
private NotificationInlineImageResolver mImageResolver;
private NotificationMediaManager mMediaManager;
- @Nullable private OnExpansionChangedListener mExpansionChangedListener;
- @Nullable private Runnable mOnIntrinsicHeightReachedRunnable;
+ @Nullable
+ private OnExpansionChangedListener mExpansionChangedListener;
+ @Nullable
+ private Runnable mOnIntrinsicHeightReachedRunnable;
private float mTopRoundnessDuringLaunchAnimation;
private float mBottomRoundnessDuringLaunchAnimation;
+ private boolean mIsNotificationGroupCornerEnabled;
/**
* Returns whether the given {@code statusBarNotification} is a system notification.
@@ -574,14 +592,18 @@
}
}
- /** Called when the notification's ranking was changed (but nothing else changed). */
+ /**
+ * Called when the notification's ranking was changed (but nothing else changed).
+ */
public void onNotificationRankingUpdated() {
if (mMenuRow != null) {
mMenuRow.onNotificationUpdated(mEntry.getSbn());
}
}
- /** Call when bubble state has changed and the button on the notification should be updated. */
+ /**
+ * Call when bubble state has changed and the button on the notification should be updated.
+ */
public void updateBubbleButton() {
for (NotificationContentView l : mLayouts) {
l.updateBubbleButton(mEntry);
@@ -620,6 +642,7 @@
/**
* Sets a supplier that can determine whether the keyguard is secure or not.
+ *
* @param secureStateProvider A function that returns true if keyguard is secure.
*/
public void setSecureStateProvider(BooleanSupplier secureStateProvider) {
@@ -781,7 +804,9 @@
mChildrenContainer.setUntruncatedChildCount(childCount);
}
- /** Called after children have been attached to set the expansion states */
+ /**
+ * Called after children have been attached to set the expansion states
+ */
public void resetChildSystemExpandedStates() {
if (isSummaryWithChildren()) {
mChildrenContainer.updateExpansionStates();
@@ -791,7 +816,7 @@
/**
* Add a child notification to this view.
*
- * @param row the row to add
+ * @param row the row to add
* @param childIndex the index to add it at, if -1 it will be added at the end
*/
public void addChildNotification(ExpandableNotificationRow row, int childIndex) {
@@ -809,10 +834,12 @@
}
onAttachedChildrenCountChanged();
row.setIsChildInGroup(false, null);
- row.setBottomRoundness(0.0f, false /* animate */);
+ row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
}
- /** Returns the child notification at [index], or null if no such child. */
+ /**
+ * Returns the child notification at [index], or null if no such child.
+ */
@Nullable
public ExpandableNotificationRow getChildNotificationAt(int index) {
if (mChildrenContainer == null
@@ -834,7 +861,7 @@
/**
* @param isChildInGroup Is this notification now in a group
- * @param parent the new parent notification
+ * @param parent the new parent notification
*/
public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {
if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) {
@@ -898,7 +925,9 @@
return mChildrenContainer == null ? null : mChildrenContainer.getAttachedChildren();
}
- /** Updates states of all children. */
+ /**
+ * Updates states of all children.
+ */
public void updateChildrenStates(AmbientState ambientState) {
if (mIsSummaryWithChildren) {
ExpandableViewState parentState = getViewState();
@@ -906,21 +935,27 @@
}
}
- /** Applies children states. */
+ /**
+ * Applies children states.
+ */
public void applyChildrenState() {
if (mIsSummaryWithChildren) {
mChildrenContainer.applyState();
}
}
- /** Prepares expansion changed. */
+ /**
+ * Prepares expansion changed.
+ */
public void prepareExpansionChanged() {
if (mIsSummaryWithChildren) {
mChildrenContainer.prepareExpansionChanged();
}
}
- /** Starts child animations. */
+ /**
+ * Starts child animations.
+ */
public void startChildAnimation(AnimationProperties properties) {
if (mIsSummaryWithChildren) {
mChildrenContainer.startAnimationToState(properties);
@@ -984,7 +1019,7 @@
if (mIsSummaryWithChildren) {
return mChildrenContainer.getIntrinsicHeight();
}
- if(mExpandedWhenPinned) {
+ if (mExpandedWhenPinned) {
return Math.max(getMaxExpandHeight(), getHeadsUpHeight());
} else if (atLeastMinHeight) {
return Math.max(getCollapsedHeight(), getHeadsUpHeight());
@@ -1079,18 +1114,22 @@
updateClickAndFocus();
}
- /** The click listener for the bubble button. */
+ /**
+ * The click listener for the bubble button.
+ */
public View.OnClickListener getBubbleClickListener() {
return v -> {
if (mBubblesManagerOptional.isPresent()) {
mBubblesManagerOptional.get()
- .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
+ .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
}
mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */);
};
}
- /** The click listener for the snooze button. */
+ /**
+ * The click listener for the snooze button.
+ */
public View.OnClickListener getSnoozeClickListener(MenuItem item) {
return v -> {
// Dismiss a snoozed notification if one is still left behind
@@ -1252,7 +1291,7 @@
}
public void setContentBackground(int customBackgroundColor, boolean animate,
- NotificationContentView notificationContentView) {
+ NotificationContentView notificationContentView) {
if (getShowingLayout() == notificationContentView) {
setTintColor(customBackgroundColor, animate);
}
@@ -1637,7 +1676,9 @@
setTargetPoint(null);
}
- /** Shows the given feedback icon, or hides the icon if null. */
+ /**
+ * Shows the given feedback icon, or hides the icon if null.
+ */
public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
if (mIsSummaryWithChildren) {
mChildrenContainer.setFeedbackIcon(icon);
@@ -1646,7 +1687,9 @@
mPublicLayout.setFeedbackIcon(icon);
}
- /** Sets the last time the notification being displayed audibly alerted the user. */
+ /**
+ * Sets the last time the notification being displayed audibly alerted the user.
+ */
public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) {
long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs;
boolean alertedRecently = timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS;
@@ -1700,7 +1743,9 @@
Trace.endSection();
}
- /** Generates and appends "(MessagingStyle)" type tag to passed string for tracing. */
+ /**
+ * Generates and appends "(MessagingStyle)" type tag to passed string for tracing.
+ */
@NonNull
private String appendTraceStyleTag(@NonNull String traceTag) {
if (!Trace.isEnabled()) {
@@ -1721,7 +1766,7 @@
super.onFinishInflate();
mPublicLayout = findViewById(R.id.expandedPublic);
mPrivateLayout = findViewById(R.id.expanded);
- mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};
+ mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
for (NotificationContentView l : mLayouts) {
l.setExpandClickListener(mExpandClickListener);
@@ -1740,6 +1785,7 @@
mChildrenContainer.setIsLowPriority(mIsLowPriority);
mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
mChildrenContainer.onNotificationUpdated();
+ mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
mTranslateableViews.add(mChildrenContainer);
});
@@ -1796,6 +1842,7 @@
/**
* Perform a smart action which triggers a longpress (expose guts).
* Based on the semanticAction passed, may update the state of the guts view.
+ *
* @param semanticAction associated with this smart action click
*/
public void doSmartActionClick(int x, int y, int semanticAction) {
@@ -1939,9 +1986,10 @@
/**
* Set the dismiss behavior of the view.
+ *
* @param usingRowTranslationX {@code true} if the view should translate using regular
- * translationX, otherwise the contents will be
- * translated.
+ * translationX, otherwise the contents will be
+ * translated.
*/
@Override
public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
@@ -1955,6 +2003,14 @@
if (previousTranslation != 0) {
setTranslation(previousTranslation);
}
+ if (mChildrenContainer != null) {
+ List<ExpandableNotificationRow> notificationChildren =
+ mChildrenContainer.getAttachedChildren();
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow child = notificationChildren.get(i);
+ child.setDismissUsingRowTranslationX(usingRowTranslationX);
+ }
+ }
}
}
@@ -2009,7 +2065,7 @@
}
public Animator getTranslateViewAnimator(final float leftTarget,
- AnimatorUpdateListener listener) {
+ AnimatorUpdateListener listener) {
if (mTranslateAnim != null) {
mTranslateAnim.cancel();
}
@@ -2115,7 +2171,7 @@
NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
float startTop = params.getStartNotificationTop();
top = (int) Math.min(MathUtils.lerp(startTop,
- params.getTop(), expandProgress),
+ params.getTop(), expandProgress),
startTop);
} else {
top = params.getTop();
@@ -2151,29 +2207,30 @@
}
setTranslationY(top);
- mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / mOutlineRadius;
- mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / mOutlineRadius;
+ final float maxRadius = getMaxRadius();
+ mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / maxRadius;
+ mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / maxRadius;
invalidateOutline();
mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight);
}
@Override
- public float getCurrentTopRoundness() {
+ public float getTopRoundness() {
if (mExpandAnimationRunning) {
return mTopRoundnessDuringLaunchAnimation;
}
- return super.getCurrentTopRoundness();
+ return super.getTopRoundness();
}
@Override
- public float getCurrentBottomRoundness() {
+ public float getBottomRoundness() {
if (mExpandAnimationRunning) {
return mBottomRoundnessDuringLaunchAnimation;
}
- return super.getCurrentBottomRoundness();
+ return super.getBottomRoundness();
}
public void setExpandAnimationRunning(boolean expandAnimationRunning) {
@@ -2284,7 +2341,7 @@
/**
* Set this notification to be expanded by the user
*
- * @param userExpanded whether the user wants this notification to be expanded
+ * @param userExpanded whether the user wants this notification to be expanded
* @param allowChildExpansion whether a call to this method allows expanding children
*/
public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
@@ -2434,7 +2491,7 @@
/**
* @return {@code true} if the notification can show it's heads up layout. This is mostly true
- * except for legacy use cases.
+ * except for legacy use cases.
*/
public boolean canShowHeadsUp() {
if (mOnKeyguard && !isDozing() && !isBypassEnabled()) {
@@ -2625,7 +2682,7 @@
@Override
public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
- long duration) {
+ long duration) {
if (getVisibility() == GONE) {
// If we are GONE, the hideSensitive parameter will not be calculated and always be
// false, which is incorrect, let's wait until a real call comes in later.
@@ -2658,9 +2715,9 @@
private void animateShowingPublic(long delay, long duration, boolean showingPublic) {
View[] privateViews = mIsSummaryWithChildren
- ? new View[] {mChildrenContainer}
- : new View[] {mPrivateLayout};
- View[] publicViews = new View[] {mPublicLayout};
+ ? new View[]{mChildrenContainer}
+ : new View[]{mPrivateLayout};
+ View[] publicViews = new View[]{mPublicLayout};
View[] hiddenChildren = showingPublic ? privateViews : publicViews;
View[] shownChildren = showingPublic ? publicViews : privateViews;
for (final View hiddenView : hiddenChildren) {
@@ -2693,8 +2750,8 @@
/**
* @return Whether this view is allowed to be dismissed. Only valid for visible notifications as
- * otherwise some state might not be updated. To request about the general clearability
- * see {@link NotificationEntry#isDismissable()}.
+ * otherwise some state might not be updated. To request about the general clearability
+ * see {@link NotificationEntry#isDismissable()}.
*/
public boolean canViewBeDismissed() {
return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral);
@@ -2777,8 +2834,13 @@
}
@Override
- public long performRemoveAnimation(long duration, long delay, float translationDirection,
- boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable,
+ public long performRemoveAnimation(
+ long duration,
+ long delay,
+ float translationDirection,
+ boolean isHeadsUpAnimation,
+ float endLocation,
+ Runnable onFinishedRunnable,
AnimatorListenerAdapter animationListener) {
if (mMenuRow != null && mMenuRow.isMenuVisible()) {
Animator anim = getTranslateViewAnimator(0f, null /* listener */);
@@ -2828,7 +2890,9 @@
}
}
- /** Gets the last value set with {@link #setNotificationFaded(boolean)} */
+ /**
+ * Gets the last value set with {@link #setNotificationFaded(boolean)}
+ */
@Override
public boolean isNotificationFaded() {
return mIsFaded;
@@ -2843,7 +2907,7 @@
* notifications return false from {@link #hasOverlappingRendering()} and delegate the
* layerType to child views which really need it in order to render correctly, such as icon
* views or the conversation face pile.
- *
+ * <p>
* Another compounding factor for notifications is that we change clipping on each frame of the
* animation, so the hardware layer isn't able to do any caching at the top level, but the
* individual elements we render with hardware layers (e.g. icons) cache wonderfully because we
@@ -2869,7 +2933,9 @@
}
}
- /** Private helper for iterating over the layouts and children containers to set faded state */
+ /**
+ * Private helper for iterating over the layouts and children containers to set faded state
+ */
private void setNotificationFadedOnChildren(boolean faded) {
delegateNotificationFaded(mChildrenContainer, faded);
for (NotificationContentView layout : mLayouts) {
@@ -2897,7 +2963,7 @@
* Because RemoteInputView is designed to be an opaque view that overlaps the Actions row, the
* row should require overlapping rendering to ensure that the overlapped view doesn't bleed
* through when alpha fading.
- *
+ * <p>
* Note that this currently works for top-level notifications which squish their height down
* while collapsing the shade, but does not work for children inside groups, because the
* accordion affect does not apply to those views, so super.hasOverlappingRendering() will
@@ -2976,7 +3042,7 @@
return mGuts.getIntrinsicHeight();
} else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp
&& mHeadsUpManager.isTrackingHeadsUp()) {
- return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
+ return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
} else if (mIsSummaryWithChildren && !isGroupExpanded() && !shouldShowPublic()) {
return mChildrenContainer.getMinHeight();
} else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp) {
@@ -3218,8 +3284,8 @@
MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext());
if (snoozeMenu != null) {
AccessibilityAction action = new AccessibilityAction(R.id.action_snooze,
- getContext().getResources()
- .getString(R.string.notification_menu_snooze_action));
+ getContext().getResources()
+ .getString(R.string.notification_menu_snooze_action));
info.addAction(action);
}
}
@@ -3280,17 +3346,17 @@
NotificationContentView contentView = (NotificationContentView) child;
if (isClippingNeeded()) {
return true;
- } else if (!hasNoRounding()
- && contentView.shouldClipToRounding(getCurrentTopRoundness() != 0.0f,
- getCurrentBottomRoundness() != 0.0f)) {
+ } else if (hasRoundedCorner()
+ && contentView.shouldClipToRounding(getTopRoundness() != 0.0f,
+ getBottomRoundness() != 0.0f)) {
return true;
}
} else if (child == mChildrenContainer) {
- if (isClippingNeeded() || !hasNoRounding()) {
+ if (isClippingNeeded() || hasRoundedCorner()) {
return true;
}
} else if (child instanceof NotificationGuts) {
- return !hasNoRounding();
+ return hasRoundedCorner();
}
return super.childNeedsClipping(child);
}
@@ -3316,14 +3382,17 @@
}
@Override
- protected void applyRoundness() {
+ public void applyRoundness() {
super.applyRoundness();
applyChildrenRoundness();
}
private void applyChildrenRoundness() {
if (mIsSummaryWithChildren) {
- mChildrenContainer.setCurrentBottomRoundness(getCurrentBottomRoundness());
+ mChildrenContainer.requestBottomRoundness(
+ getBottomRoundness(),
+ /* animate = */ false,
+ SourceType.DefaultValue);
}
}
@@ -3335,10 +3404,6 @@
return super.getCustomClipPath(child);
}
- private boolean hasNoRounding() {
- return getCurrentBottomRoundness() == 0.0f && getCurrentTopRoundness() == 0.0f;
- }
-
public boolean isMediaRow() {
return mEntry.getSbn().getNotification().isMediaNotification();
}
@@ -3434,6 +3499,7 @@
public interface LongPressListener {
/**
* Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
+ *
* @return whether the longpress was handled
*/
boolean onLongPress(View v, int x, int y, MenuItem item);
@@ -3455,6 +3521,7 @@
public interface CoordinateOnClickListener {
/**
* Equivalent to {@link View.OnClickListener#onClick(View)} with coordinates
+ *
* @return whether the click was handled
*/
boolean onClick(View v, int x, int y, MenuItem item);
@@ -3511,7 +3578,19 @@
private void setTargetPoint(Point p) {
mTargetPoint = p;
}
+
public Point getTargetPoint() {
return mTargetPoint;
}
+
+ /**
+ * Enable the support for rounded corner in notification group
+ * @param enabled true if is supported
+ */
+ public void enableNotificationGroupCorner(boolean enabled) {
+ mIsNotificationGroupCornerEnabled = enabled;
+ if (mChildrenContainer != null) {
+ mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index a493a67..842526e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -231,6 +231,8 @@
mStatusBarStateController.removeCallback(mStatusBarStateListener);
}
});
+ mView.enableNotificationGroupCorner(
+ mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER));
}
private final StatusBarStateController.StateListener mStatusBarStateListener =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index d58fe3b..4fde5d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,46 +28,21 @@
import android.view.ViewOutlineProvider;
import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.AnimatableProperty;
-import com.android.systemui.statusbar.notification.PropertyAnimator;
-import com.android.systemui.statusbar.notification.stack.AnimationProperties;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
/**
* Like {@link ExpandableView}, but setting an outline for the height and clipping.
*/
public abstract class ExpandableOutlineView extends ExpandableView {
- private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from(
- "topRoundness",
- ExpandableOutlineView::setTopRoundnessInternal,
- ExpandableOutlineView::getCurrentTopRoundness,
- R.id.top_roundess_animator_tag,
- R.id.top_roundess_animator_end_tag,
- R.id.top_roundess_animator_start_tag);
- private static final AnimatableProperty BOTTOM_ROUNDNESS = AnimatableProperty.from(
- "bottomRoundness",
- ExpandableOutlineView::setBottomRoundnessInternal,
- ExpandableOutlineView::getCurrentBottomRoundness,
- R.id.bottom_roundess_animator_tag,
- R.id.bottom_roundess_animator_end_tag,
- R.id.bottom_roundess_animator_start_tag);
- private static final AnimationProperties ROUNDNESS_PROPERTIES =
- new AnimationProperties().setDuration(
- StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS);
+ private RoundableState mRoundableState;
private static final Path EMPTY_PATH = new Path();
-
private final Rect mOutlineRect = new Rect();
- private final Path mClipPath = new Path();
private boolean mCustomOutline;
private float mOutlineAlpha = -1f;
- protected float mOutlineRadius;
private boolean mAlwaysRoundBothCorners;
private Path mTmpPath = new Path();
- private float mCurrentBottomRoundness;
- private float mCurrentTopRoundness;
- private float mBottomRoundness;
- private float mTopRoundness;
private int mBackgroundTop;
/**
@@ -80,8 +55,7 @@
private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
- if (!mCustomOutline && getCurrentTopRoundness() == 0.0f
- && getCurrentBottomRoundness() == 0.0f && !mAlwaysRoundBothCorners) {
+ if (!mCustomOutline && !hasRoundedCorner() && !mAlwaysRoundBothCorners) {
// Only when translating just the contents, does the outline need to be shifted.
int translation = !mDismissUsingRowTranslationX ? (int) getTranslation() : 0;
int left = Math.max(translation, 0);
@@ -99,14 +73,18 @@
}
};
+ @Override
+ public RoundableState getRoundableState() {
+ return mRoundableState;
+ }
+
protected Path getClipPath(boolean ignoreTranslation) {
int left;
int top;
int right;
int bottom;
int height;
- float topRoundness = mAlwaysRoundBothCorners
- ? mOutlineRadius : getCurrentBackgroundRadiusTop();
+ float topRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getTopCornerRadius();
if (!mCustomOutline) {
// The outline just needs to be shifted if we're translating the contents. Otherwise
// it's already in the right place.
@@ -130,12 +108,11 @@
if (height == 0) {
return EMPTY_PATH;
}
- float bottomRoundness = mAlwaysRoundBothCorners
- ? mOutlineRadius : getCurrentBackgroundRadiusBottom();
+ float bottomRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
if (topRoundness + bottomRoundness > height) {
float overShoot = topRoundness + bottomRoundness - height;
- float currentTopRoundness = getCurrentTopRoundness();
- float currentBottomRoundness = getCurrentBottomRoundness();
+ float currentTopRoundness = getTopRoundness();
+ float currentBottomRoundness = getBottomRoundness();
topRoundness -= overShoot * currentTopRoundness
/ (currentTopRoundness + currentBottomRoundness);
bottomRoundness -= overShoot * currentBottomRoundness
@@ -145,8 +122,18 @@
return mTmpPath;
}
- public void getRoundedRectPath(int left, int top, int right, int bottom,
- float topRoundness, float bottomRoundness, Path outPath) {
+ /**
+ * Add a round rect in {@code outPath}
+ * @param outPath destination path
+ */
+ public void getRoundedRectPath(
+ int left,
+ int top,
+ int right,
+ int bottom,
+ float topRoundness,
+ float bottomRoundness,
+ Path outPath) {
outPath.reset();
mTmpCornerRadii[0] = topRoundness;
mTmpCornerRadii[1] = topRoundness;
@@ -168,15 +155,28 @@
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
canvas.save();
+ Path clipPath = null;
+ Path childClipPath = null;
if (childNeedsClipping(child)) {
- Path clipPath = getCustomClipPath(child);
+ clipPath = getCustomClipPath(child);
if (clipPath == null) {
clipPath = getClipPath(false /* ignoreTranslation */);
}
- if (clipPath != null) {
- canvas.clipPath(clipPath);
+ // If the notification uses "RowTranslationX" as dismiss behavior, we should clip the
+ // children instead.
+ if (mDismissUsingRowTranslationX && child instanceof NotificationChildrenContainer) {
+ childClipPath = clipPath;
+ clipPath = null;
}
}
+
+ if (child instanceof NotificationChildrenContainer) {
+ ((NotificationChildrenContainer) child).setChildClipPath(childClipPath);
+ }
+ if (clipPath != null) {
+ canvas.clipPath(clipPath);
+ }
+
boolean result = super.drawChild(canvas, child, drawingTime);
canvas.restore();
return result;
@@ -207,73 +207,21 @@
private void initDimens() {
Resources res = getResources();
- mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius);
mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline);
- if (!mAlwaysRoundBothCorners) {
- mOutlineRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
+ float maxRadius;
+ if (mAlwaysRoundBothCorners) {
+ maxRadius = res.getDimension(R.dimen.notification_shadow_radius);
+ } else {
+ maxRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
}
+ mRoundableState = new RoundableState(this, this, maxRadius);
setClipToOutline(mAlwaysRoundBothCorners);
}
@Override
- public boolean setTopRoundness(float topRoundness, boolean animate) {
- if (mTopRoundness != topRoundness) {
- float diff = Math.abs(topRoundness - mTopRoundness);
- mTopRoundness = topRoundness;
- boolean shouldAnimate = animate;
- if (PropertyAnimator.isAnimating(this, TOP_ROUNDNESS) && diff > 0.5f) {
- // Fail safe:
- // when we've been animating previously and we're now getting an update in the
- // other direction, make sure to animate it too, otherwise, the localized updating
- // may make the start larger than 1.0.
- shouldAnimate = true;
- }
- PropertyAnimator.setProperty(this, TOP_ROUNDNESS, topRoundness,
- ROUNDNESS_PROPERTIES, shouldAnimate);
- return true;
- }
- return false;
- }
-
- protected void applyRoundness() {
+ public void applyRoundness() {
invalidateOutline();
- invalidate();
- }
-
- public float getCurrentBackgroundRadiusTop() {
- return getCurrentTopRoundness() * mOutlineRadius;
- }
-
- public float getCurrentTopRoundness() {
- return mCurrentTopRoundness;
- }
-
- public float getCurrentBottomRoundness() {
- return mCurrentBottomRoundness;
- }
-
- public float getCurrentBackgroundRadiusBottom() {
- return getCurrentBottomRoundness() * mOutlineRadius;
- }
-
- @Override
- public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
- if (mBottomRoundness != bottomRoundness) {
- float diff = Math.abs(bottomRoundness - mBottomRoundness);
- mBottomRoundness = bottomRoundness;
- boolean shouldAnimate = animate;
- if (PropertyAnimator.isAnimating(this, BOTTOM_ROUNDNESS) && diff > 0.5f) {
- // Fail safe:
- // when we've been animating previously and we're now getting an update in the
- // other direction, make sure to animate it too, otherwise, the localized updating
- // may make the start larger than 1.0.
- shouldAnimate = true;
- }
- PropertyAnimator.setProperty(this, BOTTOM_ROUNDNESS, bottomRoundness,
- ROUNDNESS_PROPERTIES, shouldAnimate);
- return true;
- }
- return false;
+ super.applyRoundness();
}
protected void setBackgroundTop(int backgroundTop) {
@@ -283,16 +231,6 @@
}
}
- private void setTopRoundnessInternal(float topRoundness) {
- mCurrentTopRoundness = topRoundness;
- applyRoundness();
- }
-
- private void setBottomRoundnessInternal(float bottomRoundness) {
- mCurrentBottomRoundness = bottomRoundness;
- applyRoundness();
- }
-
public void onDensityOrFontScaleChanged() {
initDimens();
applyRoundness();
@@ -348,9 +286,10 @@
/**
* Set the dismiss behavior of the view.
+ *
* @param usingRowTranslationX {@code true} if the view should translate using regular
- * translationX, otherwise the contents will be
- * translated.
+ * translationX, otherwise the contents will be
+ * translated.
*/
public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
mDismissUsingRowTranslationX = usingRowTranslationX;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 38f0c55..955d7c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -36,6 +36,8 @@
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.util.DumpUtilsKt;
@@ -47,9 +49,10 @@
/**
* An abstract view for expandable views.
*/
-public abstract class ExpandableView extends FrameLayout implements Dumpable {
+public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable {
private static final String TAG = "ExpandableView";
+ private RoundableState mRoundableState = null;
protected OnHeightChangedListener mOnHeightChangedListener;
private int mActualHeight;
protected int mClipTopAmount;
@@ -78,6 +81,14 @@
initDimens();
}
+ @Override
+ public RoundableState getRoundableState() {
+ if (mRoundableState == null) {
+ mRoundableState = new RoundableState(this, this, 0f);
+ }
+ return mRoundableState;
+ }
+
private void initDimens() {
mContentShift = getResources().getDimensionPixelSize(
R.dimen.shelf_transform_content_shift);
@@ -440,8 +451,7 @@
int top = getClipTopAmount();
int bottom = Math.max(Math.max(getActualHeight() + getExtraBottomPadding()
- mClipBottomAmount, top), mMinimumHeightForClipping);
- int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f);
- mClipRect.set(-halfExtraWidth, top, getWidth() + halfExtraWidth, bottom);
+ mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom);
setClipBounds(mClipRect);
} else {
setClipBounds(null);
@@ -455,7 +465,6 @@
public void setExtraWidthForClipping(float extraWidthForClipping) {
mExtraWidthForClipping = extraWidthForClipping;
- updateClipping();
}
public float getHeaderVisibleAmount() {
@@ -844,22 +853,6 @@
return mFirstInSection;
}
- /**
- * Set the topRoundness of this view.
- * @return Whether the roundness was changed.
- */
- public boolean setTopRoundness(float topRoundness, boolean animate) {
- return false;
- }
-
- /**
- * Set the bottom roundness of this view.
- * @return Whether the roundness was changed.
- */
- public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
- return false;
- }
-
public int getHeadsUpHeightWithoutHeader() {
return getHeight();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 8de0365..277ad8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1374,13 +1374,8 @@
if (bubbleButton == null || actionContainer == null) {
return;
}
- boolean isPersonWithShortcut =
- mPeopleIdentifier.getPeopleNotificationType(entry)
- >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
- boolean showButton = BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
- && isPersonWithShortcut
- && entry.getBubbleMetadata() != null;
- if (showButton) {
+
+ if (shouldShowBubbleButton(entry)) {
// explicitly resolve drawable resource using SystemUI's theme
Drawable d = mContext.getDrawable(entry.isBubble()
? R.drawable.bubble_ic_stop_bubble
@@ -1410,6 +1405,16 @@
}
}
+ @VisibleForTesting
+ boolean shouldShowBubbleButton(NotificationEntry entry) {
+ boolean isPersonWithShortcut =
+ mPeopleIdentifier.getPeopleNotificationType(entry)
+ >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
+ return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
+ && isPersonWithShortcut
+ && entry.getBubbleMetadata() != null;
+ }
+
private void applySnoozeAction(View layout) {
if (layout == null || mContainingNotification == null) {
return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index 7a65436..f13e48d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -35,12 +35,15 @@
import com.android.internal.widget.CachingIconView;
import com.android.internal.widget.NotificationExpandButton;
+import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.statusbar.TransformableView;
import com.android.systemui.statusbar.ViewTransformationHelper;
import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation;
import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.ImageTransformState;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
import com.android.systemui.statusbar.notification.TransformState;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -49,13 +52,12 @@
/**
* Wraps a notification view which may or may not include a header.
*/
-public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
+public class NotificationHeaderViewWrapper extends NotificationViewWrapper implements Roundable {
+ private final RoundableState mRoundableState;
private static final Interpolator LOW_PRIORITY_HEADER_CLOSE
= new PathInterpolator(0.4f, 0f, 0.7f, 1f);
-
protected final ViewTransformationHelper mTransformationHelper;
-
private CachingIconView mIcon;
private NotificationExpandButton mExpandButton;
private View mAltExpandTarget;
@@ -67,12 +69,16 @@
private ImageView mWorkProfileImage;
private View mAudiblyAlertedIcon;
private View mFeedbackIcon;
-
private boolean mIsLowPriority;
private boolean mTransformLowPriorityTitle;
protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
super(ctx, view, row);
+ mRoundableState = new RoundableState(
+ mView,
+ this,
+ ctx.getResources().getDimension(R.dimen.notification_corner_radius)
+ );
mTransformationHelper = new ViewTransformationHelper();
// we want to avoid that the header clashes with the other text when transforming
@@ -81,7 +87,8 @@
new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) {
@Override
- public Interpolator getCustomInterpolator(int interpolationType,
+ public Interpolator getCustomInterpolator(
+ int interpolationType,
boolean isFrom) {
boolean isLowPriority = mView instanceof NotificationHeaderView;
if (interpolationType == TRANSFORM_Y) {
@@ -99,11 +106,17 @@
protected boolean hasCustomTransformation() {
return mIsLowPriority && mTransformLowPriorityTitle;
}
- }, TRANSFORMING_VIEW_TITLE);
+ },
+ TRANSFORMING_VIEW_TITLE);
resolveHeaderViews();
addFeedbackOnClickListener(row);
}
+ @Override
+ public RoundableState getRoundableState() {
+ return mRoundableState;
+ }
+
protected void resolveHeaderViews() {
mIcon = mView.findViewById(com.android.internal.R.id.icon);
mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
@@ -128,7 +141,9 @@
}
}
- /** Shows the given feedback icon, or hides the icon if null. */
+ /**
+ * Shows the given feedback icon, or hides the icon if null.
+ */
@Override
public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
if (mFeedbackIcon != null) {
@@ -193,7 +208,7 @@
// its animation
&& child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) {
((ImageView) child).setCropToPadding(true);
- } else if (child instanceof ViewGroup){
+ } else if (child instanceof ViewGroup) {
ViewGroup group = (ViewGroup) child;
for (int i = 0; i < group.getChildCount(); i++) {
stack.push(group.getChildAt(i));
@@ -215,7 +230,9 @@
}
@Override
- public void updateExpandability(boolean expandable, View.OnClickListener onClickListener,
+ public void updateExpandability(
+ boolean expandable,
+ View.OnClickListener onClickListener,
boolean requestLayout) {
mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
mExpandButton.setOnClickListener(expandable ? onClickListener : null);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 7b23a56..26f0ad9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -21,6 +21,9 @@
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
import android.graphics.drawable.ColorDrawable;
import android.service.notification.StatusBarNotification;
import android.util.AttributeSet;
@@ -33,6 +36,7 @@
import android.widget.RemoteViews;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
@@ -43,10 +47,14 @@
import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.NotificationFadeAware;
import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.HybridGroupManager;
import com.android.systemui.statusbar.notification.row.HybridNotificationView;
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import java.util.ArrayList;
@@ -56,7 +64,7 @@
* A container containing child notifications
*/
public class NotificationChildrenContainer extends ViewGroup
- implements NotificationFadeAware {
+ implements NotificationFadeAware, Roundable {
private static final String TAG = "NotificationChildrenContainer";
@@ -100,9 +108,9 @@
private boolean mEnableShadowOnChildNotifications;
private NotificationHeaderView mNotificationHeader;
- private NotificationViewWrapper mNotificationHeaderWrapper;
+ private NotificationHeaderViewWrapper mNotificationHeaderWrapper;
private NotificationHeaderView mNotificationHeaderLowPriority;
- private NotificationViewWrapper mNotificationHeaderWrapperLowPriority;
+ private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority;
private NotificationGroupingUtil mGroupingUtil;
private ViewState mHeaderViewState;
private int mClipBottomAmount;
@@ -110,7 +118,8 @@
private OnClickListener mHeaderClickListener;
private ViewGroup mCurrentHeader;
private boolean mIsConversation;
-
+ private Path mChildClipPath = null;
+ private final Path mHeaderPath = new Path();
private boolean mShowGroupCountInExpander;
private boolean mShowDividersWhenExpanded;
private boolean mHideDividersDuringExpand;
@@ -119,6 +128,8 @@
private float mHeaderVisibleAmount = 1.0f;
private int mUntruncatedChildCount;
private boolean mContainingNotificationIsFaded = false;
+ private RoundableState mRoundableState;
+ private boolean mIsNotificationGroupCornerEnabled;
public NotificationChildrenContainer(Context context) {
this(context, null);
@@ -132,10 +143,14 @@
this(context, attrs, defStyleAttr, 0);
}
- public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
+ public NotificationChildrenContainer(
+ Context context,
+ AttributeSet attrs,
+ int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mHybridGroupManager = new HybridGroupManager(getContext());
+ mRoundableState = new RoundableState(this, this, 0f);
initDimens();
setClipChildren(false);
}
@@ -167,6 +182,12 @@
mHybridGroupManager.initDimens();
}
+ @NonNull
+ @Override
+ public RoundableState getRoundableState() {
+ return mRoundableState;
+ }
+
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount =
@@ -271,7 +292,7 @@
/**
* Add a child notification to this view.
*
- * @param row the row to add
+ * @param row the row to add
* @param childIndex the index to add it at, if -1 it will be added at the end
*/
public void addNotification(ExpandableNotificationRow row, int childIndex) {
@@ -347,8 +368,11 @@
mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
.setVisibility(VISIBLE);
mNotificationHeader.setOnClickListener(mHeaderClickListener);
- mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
- mNotificationHeader, mContainingNotification);
+ mNotificationHeaderWrapper =
+ (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
+ getContext(),
+ mNotificationHeader,
+ mContainingNotification);
addView(mNotificationHeader, 0);
invalidate();
} else {
@@ -381,8 +405,11 @@
mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
.setVisibility(VISIBLE);
mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
- mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(),
- mNotificationHeaderLowPriority, mContainingNotification);
+ mNotificationHeaderWrapperLowPriority =
+ (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
+ getContext(),
+ mNotificationHeaderLowPriority,
+ mContainingNotification);
addView(mNotificationHeaderLowPriority, 0);
invalidate();
} else {
@@ -461,7 +488,9 @@
return mAttachedChildren;
}
- /** To be called any time the rows have been updated */
+ /**
+ * To be called any time the rows have been updated
+ */
public void updateExpansionStates() {
if (mChildrenExpanded || mUserLocked) {
// we don't modify it the group is expanded or if we are expanding it
@@ -475,7 +504,6 @@
}
/**
- *
* @return the intrinsic size of this children container, i.e the natural fully expanded state
*/
public int getIntrinsicHeight() {
@@ -485,7 +513,7 @@
/**
* @return the intrinsic height with a number of children given
- * in @param maxAllowedVisibleChildren
+ * in @param maxAllowedVisibleChildren
*/
private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
if (showingAsLowPriority()) {
@@ -539,7 +567,8 @@
/**
* Update the state of all its children based on a linear layout algorithm.
- * @param parentState the state of the parent
+ *
+ * @param parentState the state of the parent
* @param ambientState the ambient state containing ambient information
*/
public void updateState(ExpandableViewState parentState, AmbientState ambientState) {
@@ -655,14 +684,17 @@
* When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
* height, children in the group after this are gone.
*
- * @param child the child who's height to adjust.
+ * @param child the child who's height to adjust.
* @param parentHeight the height of the parent.
- * @param childState the state to update.
- * @param yPosition the yPosition of the view.
+ * @param childState the state to update.
+ * @param yPosition the yPosition of the view.
* @return true if children after this one should be hidden.
*/
- private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
- int parentHeight, ExpandableViewState childState, int yPosition) {
+ private boolean updateChildStateForExpandedGroup(
+ ExpandableNotificationRow child,
+ int parentHeight,
+ ExpandableViewState childState,
+ int yPosition) {
final int top = yPosition + child.getClipTopAmount();
final int intrinsicHeight = child.getIntrinsicHeight();
final int bottom = top + intrinsicHeight;
@@ -690,13 +722,15 @@
if (mIsLowPriority
|| (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
|| (mContainingNotification.isHeadsUpState()
- && mContainingNotification.canShowHeadsUp())) {
+ && mContainingNotification.canShowHeadsUp())) {
return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
}
return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
}
- /** Applies state to children. */
+ /**
+ * Applies state to children.
+ */
public void applyState() {
int childCount = mAttachedChildren.size();
ViewState tmpState = new ViewState();
@@ -768,17 +802,73 @@
}
}
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ boolean isCanvasChanged = false;
+
+ Path clipPath = mChildClipPath;
+ if (clipPath != null) {
+ final float translation;
+ if (child instanceof ExpandableNotificationRow) {
+ ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child;
+ translation = notificationRow.getTranslation();
+ } else {
+ translation = child.getTranslationX();
+ }
+
+ isCanvasChanged = true;
+ canvas.save();
+ if (mIsNotificationGroupCornerEnabled && translation != 0f) {
+ clipPath.offset(translation, 0f);
+ canvas.clipPath(clipPath);
+ clipPath.offset(-translation, 0f);
+ } else {
+ canvas.clipPath(clipPath);
+ }
+ }
+
+ if (child instanceof NotificationHeaderView
+ && mNotificationHeaderWrapper.hasRoundedCorner()) {
+ float[] radii = mNotificationHeaderWrapper.getUpdatedRadii();
+ mHeaderPath.reset();
+ mHeaderPath.addRoundRect(
+ child.getLeft(),
+ child.getTop(),
+ child.getRight(),
+ child.getBottom(),
+ radii,
+ Direction.CW
+ );
+ if (!isCanvasChanged) {
+ isCanvasChanged = true;
+ canvas.save();
+ }
+ canvas.clipPath(mHeaderPath);
+ }
+
+ if (isCanvasChanged) {
+ boolean result = super.drawChild(canvas, child, drawingTime);
+ canvas.restore();
+ return result;
+ } else {
+ // If there have been no changes to the canvas we can proceed as usual
+ return super.drawChild(canvas, child, drawingTime);
+ }
+ }
+
+
/**
* This is called when the children expansion has changed and positions the children properly
* for an appear animation.
- *
*/
public void prepareExpansionChanged() {
// TODO: do something that makes sense, like placing the invisible views correctly
return;
}
- /** Animate to a given state. */
+ /**
+ * Animate to a given state.
+ */
public void startAnimationToState(AnimationProperties properties) {
int childCount = mAttachedChildren.size();
ViewState tmpState = new ViewState();
@@ -1102,7 +1192,8 @@
* Get the minimum Height for this group.
*
* @param maxAllowedVisibleChildren the number of children that should be visible
- * @param likeHighPriority if the height should be calculated as if it were not low priority
+ * @param likeHighPriority if the height should be calculated as if it were not low
+ * priority
*/
private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation);
@@ -1112,10 +1203,13 @@
* Get the minimum Height for this group.
*
* @param maxAllowedVisibleChildren the number of children that should be visible
- * @param likeHighPriority if the height should be calculated as if it were not low priority
- * @param headerTranslation the translation amount of the header
+ * @param likeHighPriority if the height should be calculated as if it were not low
+ * priority
+ * @param headerTranslation the translation amount of the header
*/
- private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority,
+ private int getMinHeight(
+ int maxAllowedVisibleChildren,
+ boolean likeHighPriority,
int headerTranslation) {
if (!likeHighPriority && showingAsLowPriority()) {
if (mNotificationHeaderLowPriority == null) {
@@ -1274,16 +1368,19 @@
return mUserLocked;
}
- public void setCurrentBottomRoundness(float currentBottomRoundness) {
+ @Override
+ public void applyRoundness() {
+ Roundable.super.applyRoundness();
boolean last = true;
for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
ExpandableNotificationRow child = mAttachedChildren.get(i);
if (child.getVisibility() == View.GONE) {
continue;
}
- float bottomRoundness = last ? currentBottomRoundness : 0.0f;
- child.setBottomRoundness(bottomRoundness, isShown() /* animate */);
- child.setTopRoundness(0.0f, false /* animate */);
+ child.requestBottomRoundness(
+ last ? getBottomRoundness() : 0f,
+ /* animate = */ isShown(),
+ SourceType.DefaultValue);
last = false;
}
}
@@ -1293,7 +1390,9 @@
mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader);
}
- /** Shows the given feedback icon, or hides the icon if null. */
+ /**
+ * Shows the given feedback icon, or hides the icon if null.
+ */
public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
if (mNotificationHeaderWrapper != null) {
mNotificationHeaderWrapper.setFeedbackIcon(icon);
@@ -1325,4 +1424,26 @@
child.setNotificationFaded(faded);
}
}
+
+ /**
+ * Allow to define a path the clip the children in #drawChild()
+ *
+ * @param childClipPath path used to clip the children
+ */
+ public void setChildClipPath(@Nullable Path childClipPath) {
+ mChildClipPath = childClipPath;
+ invalidate();
+ }
+
+ public NotificationHeaderViewWrapper getNotificationHeaderWrapper() {
+ return mNotificationHeaderWrapper;
+ }
+
+ /**
+ * Enable the support for rounded corner in notification group
+ * @param enabled true if is supported
+ */
+ public void enableNotificationGroupCorner(boolean enabled) {
+ mIsNotificationGroupCornerEnabled = enabled;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
index 2015c87..6810055 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
@@ -26,6 +26,8 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -59,8 +61,8 @@
private boolean mIsClearAllInProgress;
private ExpandableView mSwipedView = null;
- private ExpandableView mViewBeforeSwipedView = null;
- private ExpandableView mViewAfterSwipedView = null;
+ private Roundable mViewBeforeSwipedView = null;
+ private Roundable mViewAfterSwipedView = null;
@Inject
NotificationRoundnessManager(
@@ -101,11 +103,12 @@
public boolean isViewAffectedBySwipe(ExpandableView expandableView) {
return expandableView != null
&& (expandableView == mSwipedView
- || expandableView == mViewBeforeSwipedView
- || expandableView == mViewAfterSwipedView);
+ || expandableView == mViewBeforeSwipedView
+ || expandableView == mViewAfterSwipedView);
}
- boolean updateViewWithoutCallback(ExpandableView view,
+ boolean updateViewWithoutCallback(
+ ExpandableView view,
boolean animate) {
if (view == null
|| view == mViewBeforeSwipedView
@@ -113,11 +116,15 @@
return false;
}
- final float topRoundness = getRoundnessFraction(view, true /* top */);
- final float bottomRoundness = getRoundnessFraction(view, false /* top */);
+ final boolean isTopChanged = view.requestTopRoundness(
+ getRoundnessDefaultValue(view, true /* top */),
+ animate,
+ SourceType.DefaultValue);
- final boolean topChanged = view.setTopRoundness(topRoundness, animate);
- final boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate);
+ final boolean isBottomChanged = view.requestBottomRoundness(
+ getRoundnessDefaultValue(view, /* top = */ false),
+ animate,
+ SourceType.DefaultValue);
final boolean isFirstInSection = isFirstInSection(view);
final boolean isLastInSection = isLastInSection(view);
@@ -126,9 +133,9 @@
view.setLastInSection(isLastInSection);
mNotifLogger.onCornersUpdated(view, isFirstInSection,
- isLastInSection, topChanged, bottomChanged);
+ isLastInSection, isTopChanged, isBottomChanged);
- return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged);
+ return (isFirstInSection || isLastInSection) && (isTopChanged || isBottomChanged);
}
private boolean isFirstInSection(ExpandableView view) {
@@ -150,42 +157,46 @@
}
void setViewsAffectedBySwipe(
- ExpandableView viewBefore,
+ Roundable viewBefore,
ExpandableView viewSwiped,
- ExpandableView viewAfter) {
+ Roundable viewAfter) {
final boolean animate = true;
+ final SourceType source = SourceType.OnDismissAnimation;
- ExpandableView oldViewBefore = mViewBeforeSwipedView;
+ // This method requires you to change the roundness of the current View targets and reset
+ // the roundness of the old View targets (if any) to 0f.
+ // To avoid conflicts, it generates a set of old Views and removes the current Views
+ // from this set.
+ HashSet<Roundable> oldViews = new HashSet<>();
+ if (mViewBeforeSwipedView != null) oldViews.add(mViewBeforeSwipedView);
+ if (mSwipedView != null) oldViews.add(mSwipedView);
+ if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView);
+
mViewBeforeSwipedView = viewBefore;
- if (oldViewBefore != null) {
- final float bottomRoundness = getRoundnessFraction(oldViewBefore, false /* top */);
- oldViewBefore.setBottomRoundness(bottomRoundness, animate);
- }
if (viewBefore != null) {
- viewBefore.setBottomRoundness(1f, animate);
+ oldViews.remove(viewBefore);
+ viewBefore.requestTopRoundness(0f, animate, source);
+ viewBefore.requestBottomRoundness(1f, animate, source);
}
- ExpandableView oldSwipedview = mSwipedView;
mSwipedView = viewSwiped;
- if (oldSwipedview != null) {
- final float bottomRoundness = getRoundnessFraction(oldSwipedview, false /* top */);
- final float topRoundness = getRoundnessFraction(oldSwipedview, true /* top */);
- oldSwipedview.setTopRoundness(topRoundness, animate);
- oldSwipedview.setBottomRoundness(bottomRoundness, animate);
- }
if (viewSwiped != null) {
- viewSwiped.setTopRoundness(1f, animate);
- viewSwiped.setBottomRoundness(1f, animate);
+ oldViews.remove(viewSwiped);
+ viewSwiped.requestTopRoundness(1f, animate, source);
+ viewSwiped.requestBottomRoundness(1f, animate, source);
}
- ExpandableView oldViewAfter = mViewAfterSwipedView;
mViewAfterSwipedView = viewAfter;
- if (oldViewAfter != null) {
- final float topRoundness = getRoundnessFraction(oldViewAfter, true /* top */);
- oldViewAfter.setTopRoundness(topRoundness, animate);
- }
if (viewAfter != null) {
- viewAfter.setTopRoundness(1f, animate);
+ oldViews.remove(viewAfter);
+ viewAfter.requestTopRoundness(1f, animate, source);
+ viewAfter.requestBottomRoundness(0f, animate, source);
+ }
+
+ // After setting the current Views, reset the views that are still present in the set.
+ for (Roundable oldView : oldViews) {
+ oldView.requestTopRoundness(0f, animate, source);
+ oldView.requestBottomRoundness(0f, animate, source);
}
}
@@ -193,7 +204,7 @@
mIsClearAllInProgress = isClearingAll;
}
- private float getRoundnessFraction(ExpandableView view, boolean top) {
+ private float getRoundnessDefaultValue(Roundable view, boolean top) {
if (view == null) {
return 0f;
}
@@ -207,28 +218,35 @@
&& mIsClearAllInProgress) {
return 1.0f;
}
- if ((view.isPinned()
- || (view.isHeadsUpAnimatingAway()) && !mExpanded)) {
- return 1.0f;
- }
- if (isFirstInSection(view) && top) {
- return 1.0f;
- }
- if (isLastInSection(view) && !top) {
- return 1.0f;
- }
+ if (view instanceof ExpandableView) {
+ ExpandableView expandableView = (ExpandableView) view;
+ if ((expandableView.isPinned()
+ || (expandableView.isHeadsUpAnimatingAway()) && !mExpanded)) {
+ return 1.0f;
+ }
+ if (isFirstInSection(expandableView) && top) {
+ return 1.0f;
+ }
+ if (isLastInSection(expandableView) && !top) {
+ return 1.0f;
+ }
- if (view == mTrackedHeadsUp) {
- // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be
- // rounded.
- return MathUtils.saturate(1.0f - mAppearFraction);
+ if (view == mTrackedHeadsUp) {
+ // If we're pushing up on a headsup the appear fraction is < 0 and it needs to
+ // still be rounded.
+ return MathUtils.saturate(1.0f - mAppearFraction);
+ }
+ if (expandableView.showingPulsing() && mRoundForPulsingViews) {
+ return 1.0f;
+ }
+ if (expandableView.isChildInGroup()) {
+ return 0f;
+ }
+ final Resources resources = expandableView.getResources();
+ return resources.getDimension(R.dimen.notification_corner_radius_small)
+ / resources.getDimension(R.dimen.notification_corner_radius);
}
- if (view.showingPulsing() && mRoundForPulsingViews) {
- return 1.0f;
- }
- final Resources resources = view.getResources();
- return resources.getDimension(R.dimen.notification_corner_radius_small)
- / resources.getDimension(R.dimen.notification_corner_radius);
+ return 0f;
}
public void setExpanded(float expandedHeight, float appearFraction) {
@@ -258,8 +276,10 @@
mNotifLogger.onSectionCornersUpdated(sections, anyChanged);
}
- private boolean handleRemovedOldViews(NotificationSection[] sections,
- ExpandableView[] oldViews, boolean first) {
+ private boolean handleRemovedOldViews(
+ NotificationSection[] sections,
+ ExpandableView[] oldViews,
+ boolean first) {
boolean anyChanged = false;
for (ExpandableView oldView : oldViews) {
if (oldView != null) {
@@ -289,8 +309,10 @@
return anyChanged;
}
- private boolean handleAddedNewViews(NotificationSection[] sections,
- ExpandableView[] oldViews, boolean first) {
+ private boolean handleAddedNewViews(
+ NotificationSection[] sections,
+ ExpandableView[] oldViews,
+ boolean first) {
boolean anyChanged = false;
for (NotificationSection section : sections) {
ExpandableView newView =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 2272411..df705c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1188,7 +1188,7 @@
return;
}
for (int i = 0; i < getChildCount(); i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (mChildrenToAddAnimated.contains(child)) {
final int startingPosition = getPositionInLinearLayout(child);
final int childHeight = getIntrinsicHeight(child) + mPaddingBetweenElements;
@@ -1658,7 +1658,7 @@
// find the view under the pointer, accounting for GONE views
final int count = getChildCount();
for (int childIdx = 0; childIdx < count; childIdx++) {
- ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
+ ExpandableView slidingChild = getChildAtIndex(childIdx);
if (slidingChild.getVisibility() != VISIBLE
|| (ignoreDecors && slidingChild instanceof StackScrollerDecorView)) {
continue;
@@ -1691,6 +1691,10 @@
return null;
}
+ private ExpandableView getChildAtIndex(int index) {
+ return (ExpandableView) getChildAt(index);
+ }
+
public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
getLocationOnScreen(mTempInt2);
return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
@@ -2276,7 +2280,7 @@
int childCount = getChildCount();
int count = 0;
for (int i = 0; i < childCount; i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) {
count++;
}
@@ -2496,7 +2500,7 @@
private ExpandableView getLastChildWithBackground() {
int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
&& child != mShelf) {
return child;
@@ -2509,7 +2513,7 @@
private ExpandableView getFirstChildWithBackground() {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
&& child != mShelf) {
return child;
@@ -2523,7 +2527,7 @@
ArrayList<ExpandableView> children = new ArrayList<>();
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (child.getVisibility() != View.GONE
&& !(child instanceof StackScrollerDecorView)
&& child != mShelf) {
@@ -2882,7 +2886,7 @@
}
int position = 0;
for (int i = 0; i < getChildCount(); i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
boolean notGone = child.getVisibility() != View.GONE;
if (notGone && !child.hasNoContentHeight()) {
if (position != 0) {
@@ -2936,7 +2940,7 @@
}
mAmbientState.setLastVisibleBackgroundChild(lastChild);
// TODO: Refactor SectionManager and put the RoundnessManager there.
- mController.getNoticationRoundessManager().updateRoundedChildren(mSections);
+ mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
mAnimateBottomOnLayout = false;
invalidate();
}
@@ -3968,7 +3972,7 @@
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private void clearUserLockedViews() {
for (int i = 0; i < getChildCount(); i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
row.setUserLocked(false);
@@ -3981,7 +3985,7 @@
// lets make sure nothing is transient anymore
clearTemporaryViewsInGroup(this);
for (int i = 0; i < getChildCount(); i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
clearTemporaryViewsInGroup(row.getChildrenContainer());
@@ -4230,7 +4234,7 @@
if (hideSensitive != mAmbientState.isHideSensitive()) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
- ExpandableView v = (ExpandableView) getChildAt(i);
+ ExpandableView v = getChildAtIndex(i);
v.setHideSensitiveForIntrinsicHeight(hideSensitive);
}
mAmbientState.setHideSensitive(hideSensitive);
@@ -4265,7 +4269,7 @@
private void applyCurrentState() {
int numChildren = getChildCount();
for (int i = 0; i < numChildren; i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
child.applyViewState();
}
@@ -4285,7 +4289,7 @@
// Lefts first sort by Z difference
for (int i = 0; i < getChildCount(); i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (child.getVisibility() != GONE) {
mTmpSortedChildren.add(child);
}
@@ -4512,7 +4516,7 @@
public void setClearAllInProgress(boolean clearAllInProgress) {
mClearAllInProgress = clearAllInProgress;
mAmbientState.setClearAllInProgress(clearAllInProgress);
- mController.getNoticationRoundessManager().setClearAllInProgress(clearAllInProgress);
+ mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress);
}
boolean getClearAllInProgress() {
@@ -4555,7 +4559,7 @@
final int count = getChildCount();
float max = 0;
for (int childIdx = 0; childIdx < count; childIdx++) {
- ExpandableView child = (ExpandableView) getChildAt(childIdx);
+ ExpandableView child = getChildAtIndex(childIdx);
if (child.getVisibility() == GONE) {
continue;
}
@@ -4586,7 +4590,7 @@
public boolean isBelowLastNotification(float touchX, float touchY) {
int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
if (child.getVisibility() != View.GONE) {
float childTop = child.getY();
if (childTop > touchY) {
@@ -5052,7 +5056,7 @@
pw.println();
for (int i = 0; i < childCount; i++) {
- ExpandableView child = (ExpandableView) getChildAt(i);
+ ExpandableView child = getChildAtIndex(i);
child.dump(pw, args);
pw.println();
}
@@ -5341,7 +5345,7 @@
float wakeUplocation = -1f;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
- ExpandableView view = (ExpandableView) getChildAt(i);
+ ExpandableView view = getChildAtIndex(i);
if (view.getVisibility() == View.GONE) {
continue;
}
@@ -5380,7 +5384,7 @@
public void setController(
NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
mController = notificationStackScrollLayoutController;
- mController.getNoticationRoundessManager().setAnimatedChildren(mChildrenToAddAnimated);
+ mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated);
}
void addSwipedOutView(View v) {
@@ -5391,31 +5395,22 @@
if (!(viewSwiped instanceof ExpandableNotificationRow)) {
return;
}
- final int indexOfSwipedView = indexOfChild(viewSwiped);
- if (indexOfSwipedView < 0) {
- return;
- }
mSectionsManager.updateFirstAndLastViewsForAllSections(
- mSections, getChildrenWithBackground());
- View viewBefore = null;
- if (indexOfSwipedView > 0) {
- viewBefore = getChildAt(indexOfSwipedView - 1);
- if (mSectionsManager.beginsSection(viewSwiped, viewBefore)) {
- viewBefore = null;
- }
- }
- View viewAfter = null;
- if (indexOfSwipedView < getChildCount()) {
- viewAfter = getChildAt(indexOfSwipedView + 1);
- if (mSectionsManager.beginsSection(viewAfter, viewSwiped)) {
- viewAfter = null;
- }
- }
- mController.getNoticationRoundessManager()
+ mSections,
+ getChildrenWithBackground()
+ );
+
+ RoundableTargets targets = mController.getNotificationTargetsHelper().findRoundableTargets(
+ (ExpandableNotificationRow) viewSwiped,
+ this,
+ mSectionsManager
+ );
+
+ mController.getNotificationRoundnessManager()
.setViewsAffectedBySwipe(
- (ExpandableView) viewBefore,
- (ExpandableView) viewSwiped,
- (ExpandableView) viewAfter);
+ targets.getBefore(),
+ targets.getSwiped(),
+ targets.getAfter());
updateFirstAndLastBackgroundViews();
requestDisallowInterceptTouchEvent(true);
@@ -5426,7 +5421,7 @@
void onSwipeEnd() {
updateFirstAndLastBackgroundViews();
- mController.getNoticationRoundessManager()
+ mController.getNotificationRoundnessManager()
.setViewsAffectedBySwipe(null, null, null);
// Round bottom corners for notification right before shelf.
mShelf.updateAppearance();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index bde98ff..e1337826 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -180,6 +180,7 @@
private int mBarState;
private HeadsUpAppearanceController mHeadsUpAppearanceController;
private final FeatureFlags mFeatureFlags;
+ private final NotificationTargetsHelper mNotificationTargetsHelper;
private View mLongPressedView;
@@ -642,7 +643,8 @@
StackStateLogger stackLogger,
NotificationStackScrollLogger logger,
NotificationStackSizeCalculator notificationStackSizeCalculator,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ NotificationTargetsHelper notificationTargetsHelper) {
mStackStateLogger = stackLogger;
mLogger = logger;
mAllowLongPress = allowLongPress;
@@ -679,6 +681,7 @@
mRemoteInputManager = remoteInputManager;
mShadeController = shadeController;
mFeatureFlags = featureFlags;
+ mNotificationTargetsHelper = notificationTargetsHelper;
updateResources();
}
@@ -1380,7 +1383,7 @@
return mView.calculateGapHeight(previousView, child, count);
}
- NotificationRoundnessManager getNoticationRoundessManager() {
+ NotificationRoundnessManager getNotificationRoundnessManager() {
return mNotificationRoundnessManager;
}
@@ -1537,6 +1540,10 @@
mNotificationActivityStarter = activityStarter;
}
+ public NotificationTargetsHelper getNotificationTargetsHelper() {
+ return mNotificationTargetsHelper;
+ }
+
/**
* Enum for UiEvent logged from this class
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
new file mode 100644
index 0000000..991a14b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
@@ -0,0 +1,100 @@
+package com.android.systemui.statusbar.notification.stack
+
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.Roundable
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import javax.inject.Inject
+
+/**
+ * Utility class that helps us find the targets of an animation, often used to find the notification
+ * ([Roundable]) above and below the current one (see [findRoundableTargets]).
+ */
+@SysUISingleton
+class NotificationTargetsHelper
+@Inject
+constructor(
+ featureFlags: FeatureFlags,
+) {
+ private val isNotificationGroupCornerEnabled =
+ featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)
+
+ /**
+ * This method looks for views that can be rounded (and implement [Roundable]) during a
+ * notification swipe.
+ * @return The [Roundable] targets above/below the [viewSwiped] (if available). The
+ * [RoundableTargets.before] and [RoundableTargets.after] parameters can be `null` if there is
+ * no above/below notification or the notification is not part of the same section.
+ */
+ fun findRoundableTargets(
+ viewSwiped: ExpandableNotificationRow,
+ stackScrollLayout: NotificationStackScrollLayout,
+ sectionsManager: NotificationSectionsManager,
+ ): RoundableTargets {
+ val viewBefore: Roundable?
+ val viewAfter: Roundable?
+
+ val notificationParent = viewSwiped.notificationParent
+ val childrenContainer = notificationParent?.childrenContainer
+ val visibleStackChildren =
+ stackScrollLayout.children
+ .filterIsInstance<ExpandableView>()
+ .filter { it.isVisible }
+ .toList()
+ if (notificationParent != null && childrenContainer != null) {
+ // We are inside a notification group
+
+ if (!isNotificationGroupCornerEnabled) {
+ return RoundableTargets(null, null, null)
+ }
+
+ val visibleGroupChildren = childrenContainer.attachedChildren.filter { it.isVisible }
+ val indexOfParentSwipedView = visibleGroupChildren.indexOf(viewSwiped)
+
+ viewBefore =
+ visibleGroupChildren.getOrNull(indexOfParentSwipedView - 1)
+ ?: childrenContainer.notificationHeaderWrapper
+
+ viewAfter =
+ visibleGroupChildren.getOrNull(indexOfParentSwipedView + 1)
+ ?: visibleStackChildren.indexOf(notificationParent).let {
+ visibleStackChildren.getOrNull(it + 1)
+ }
+ } else {
+ // Assumption: we are inside the NotificationStackScrollLayout
+
+ val indexOfSwipedView = visibleStackChildren.indexOf(viewSwiped)
+
+ viewBefore =
+ visibleStackChildren.getOrNull(indexOfSwipedView - 1)?.takeIf {
+ !sectionsManager.beginsSection(viewSwiped, it)
+ }
+
+ viewAfter =
+ visibleStackChildren.getOrNull(indexOfSwipedView + 1)?.takeIf {
+ !sectionsManager.beginsSection(it, viewSwiped)
+ }
+ }
+
+ return RoundableTargets(
+ before = viewBefore,
+ swiped = viewSwiped,
+ after = viewAfter,
+ )
+ }
+}
+
+/**
+ * This object contains targets above/below the [swiped] (if available). The [before] and [after]
+ * parameters can be `null` if there is no above/below notification or the notification is not part
+ * of the same section.
+ */
+data class RoundableTargets(
+ val before: Roundable?,
+ val swiped: ExpandableNotificationRow?,
+ val after: Roundable?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 0502159..eea1d911 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -31,6 +31,7 @@
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -804,7 +805,7 @@
row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
- row.setBottomRoundness(roundness, /* animate= */ false);
+ row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll);
}
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index da6d455..dd400b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -47,6 +47,7 @@
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
+import android.view.ViewRootImpl;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowInsetsController;
@@ -61,6 +62,8 @@
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -88,6 +91,7 @@
*/
public class RemoteInputView extends LinearLayout implements View.OnClickListener {
+ private static final boolean DEBUG = false;
private static final String TAG = "RemoteInput";
// A marker object that let's us easily find views of this class.
@@ -124,6 +128,7 @@
// TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
// that need the controller shouldn't have access to the view
private RemoteInputViewController mViewController;
+ private ViewRootImpl mTestableViewRootImpl;
/**
* Enum for logged notification remote input UiEvents.
@@ -430,10 +435,20 @@
}
}
+ @VisibleForTesting
+ protected void setViewRootImpl(ViewRootImpl viewRoot) {
+ mTestableViewRootImpl = viewRoot;
+ }
+
+ @VisibleForTesting
+ protected void setEditTextReferenceToSelf() {
+ mEditText.mRemoteInputView = this;
+ }
+
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- mEditText.mRemoteInputView = this;
+ setEditTextReferenceToSelf();
mEditText.setOnEditorActionListener(mEditorActionHandler);
mEditText.addTextChangedListener(mTextWatcher);
if (mEntry.getRow().isChangingPosition()) {
@@ -457,7 +472,50 @@
}
@Override
+ public ViewRootImpl getViewRootImpl() {
+ if (mTestableViewRootImpl != null) {
+ return mTestableViewRootImpl;
+ }
+ return super.getViewRootImpl();
+ }
+
+ private void registerBackCallback() {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot == null) {
+ if (DEBUG) {
+ Log.d(TAG, "ViewRoot was null, NOT registering Predictive Back callback");
+ }
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "registering Predictive Back callback");
+ }
+ viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher.PRIORITY_OVERLAY, mEditText.mOnBackInvokedCallback);
+ }
+
+ private void unregisterBackCallback() {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot == null) {
+ if (DEBUG) {
+ Log.d(TAG, "ViewRoot was null, NOT unregistering Predictive Back callback");
+ }
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "unregistering Predictive Back callback");
+ }
+ viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(
+ mEditText.mOnBackInvokedCallback);
+ }
+
+ @Override
public void onVisibilityAggregated(boolean isVisible) {
+ if (isVisible) {
+ registerBackCallback();
+ } else {
+ unregisterBackCallback();
+ }
super.onVisibilityAggregated(isVisible);
mEditText.setEnabled(isVisible && !mSending);
}
@@ -822,10 +880,21 @@
return super.onKeyDown(keyCode, event);
}
+ private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
+ if (DEBUG) {
+ Log.d(TAG, "Predictive Back Callback dispatched");
+ }
+ respondToKeycodeBack();
+ };
+
+ private void respondToKeycodeBack() {
+ defocusIfNeeded(true /* animate */);
+ }
+
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
- defocusIfNeeded(true /* animate */);
+ respondToKeycodeBack();
return true;
}
return super.onKeyUp(keyCode, event);
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index d5d904c..f0a50de 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -17,7 +17,6 @@
package com.android.systemui.temporarydisplay
import android.annotation.LayoutRes
-import android.annotation.SuppressLint
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.Rect
@@ -67,11 +66,10 @@
* Window layout params that will be used as a starting point for the [windowLayoutParams] of
* all subclasses.
*/
- @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY
internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
- type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
+ type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
title = windowTitle
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 1a8aafb..cd7bd2d 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -124,6 +124,9 @@
// ---- Text ----
val textView = currentView.requireViewById<TextView>(R.id.text)
TextViewBinder.bind(textView, newInfo.text)
+ // Updates text view bounds to make sure it perfectly fits the new text
+ // (If the new text is smaller than the previous text) see b/253228632.
+ textView.requestLayout()
// ---- End item ----
// Loading
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
new file mode 100644
index 0000000..9653985
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.util.kotlin
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import java.util.function.Consumer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to
+ * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners
+ * during onViewAttached() and removing during onViewRemoved()
+ */
+@JvmOverloads
+fun <T> collectFlow(
+ view: View,
+ flow: Flow<T>,
+ consumer: Consumer<T>,
+ state: Lifecycle.State = Lifecycle.State.CREATED,
+) {
+ view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index fbc6a58..309f168 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -22,6 +22,7 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -55,6 +56,8 @@
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.systemui.tracing.nano.SystemUiTraceProto;
+import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.nano.WmShellTraceProto;
import com.android.wm.shell.onehanded.OneHanded;
@@ -111,6 +114,7 @@
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<OneHanded> mOneHandedOptional;
private final Optional<FloatingTasks> mFloatingTasksOptional;
+ private final Optional<DesktopMode> mDesktopModeOptional;
private final CommandQueue mCommandQueue;
private final ConfigurationController mConfigurationController;
@@ -173,6 +177,7 @@
Optional<SplitScreen> splitScreenOptional,
Optional<OneHanded> oneHandedOptional,
Optional<FloatingTasks> floatingTasksOptional,
+ Optional<DesktopMode> desktopMode,
CommandQueue commandQueue,
ConfigurationController configurationController,
KeyguardStateController keyguardStateController,
@@ -194,6 +199,7 @@
mPipOptional = pipOptional;
mSplitScreenOptional = splitScreenOptional;
mOneHandedOptional = oneHandedOptional;
+ mDesktopModeOptional = desktopMode;
mWakefulnessLifecycle = wakefulnessLifecycle;
mProtoTracer = protoTracer;
mUserTracker = userTracker;
@@ -219,6 +225,7 @@
mPipOptional.ifPresent(this::initPip);
mSplitScreenOptional.ifPresent(this::initSplitScreen);
mOneHandedOptional.ifPresent(this::initOneHanded);
+ mDesktopModeOptional.ifPresent(this::initDesktopMode);
}
@VisibleForTesting
@@ -326,6 +333,16 @@
});
}
+ void initDesktopMode(DesktopMode desktopMode) {
+ desktopMode.addListener(new DesktopModeTaskRepository.VisibleTasksListener() {
+ @Override
+ public void onVisibilityChanged(boolean hasFreeformTasks) {
+ mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks)
+ .commitUpdate(DEFAULT_DISPLAY);
+ }
+ }, mSysUiMainExecutor);
+ }
+
@Override
public void writeToProto(SystemUiTraceProto proto) {
if (proto.wmShell == null) {
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index ba28045..1b404a8 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -88,6 +88,11 @@
android:excludeFromRecents="true"
/>
+ <activity android:name=".settings.brightness.BrightnessDialogTest$TestDialog"
+ android:exported="false"
+ android:excludeFromRecents="true"
+ />
+
<activity android:name="com.android.systemui.screenshot.ScrollViewActivity"
android:exported="false" />
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 03efd06..1c3656d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.mockito.any
@@ -82,7 +83,7 @@
@Mock private lateinit var parentView: View
@Mock private lateinit var transitionRepository: KeyguardTransitionRepository
private lateinit var repository: FakeKeyguardRepository
-
+ @Mock private lateinit var logBuffer: LogBuffer
private lateinit var underTest: ClockEventController
@Before
@@ -109,6 +110,7 @@
context,
mainExecutor,
bgExecutor,
+ logBuffer,
featureFlags
)
underTest.clock = clock
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
new file mode 100644
index 0000000..ae8f419
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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 static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimatedStateListDrawable;
+import android.util.Pair;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.doze.util.BurnInHelperKt;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class LockIconViewControllerBaseTest extends SysuiTestCase {
+ protected static final String UNLOCKED_LABEL = "unlocked";
+ protected static final int PADDING = 10;
+
+ protected MockitoSession mStaticMockSession;
+
+ protected @Mock LockIconView mLockIconView;
+ protected @Mock AnimatedStateListDrawable mIconDrawable;
+ protected @Mock Context mContext;
+ protected @Mock Resources mResources;
+ protected @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager;
+ protected @Mock StatusBarStateController mStatusBarStateController;
+ protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ protected @Mock KeyguardViewController mKeyguardViewController;
+ protected @Mock KeyguardStateController mKeyguardStateController;
+ protected @Mock FalsingManager mFalsingManager;
+ protected @Mock AuthController mAuthController;
+ protected @Mock DumpManager mDumpManager;
+ protected @Mock AccessibilityManager mAccessibilityManager;
+ protected @Mock ConfigurationController mConfigurationController;
+ protected @Mock VibratorHelper mVibrator;
+ protected @Mock AuthRippleController mAuthRippleController;
+ protected @Mock FeatureFlags mFeatureFlags;
+ protected @Mock KeyguardTransitionRepository mTransitionRepository;
+ protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
+
+ protected LockIconViewController mUnderTest;
+
+ // Capture listeners so that they can be used to send events
+ @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
+ ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+
+ @Captor protected ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
+ ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+ protected KeyguardStateController.Callback mKeyguardStateCallback;
+
+ @Captor protected ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
+ ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+ protected StatusBarStateController.StateListener mStatusBarStateListener;
+
+ @Captor protected ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
+ protected AuthController.Callback mAuthControllerCallback;
+
+ @Captor protected ArgumentCaptor<KeyguardUpdateMonitorCallback>
+ mKeyguardUpdateMonitorCallbackCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ protected KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
+
+ @Captor protected ArgumentCaptor<Point> mPointCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ mStaticMockSession = mockitoSession()
+ .mockStatic(BurnInHelperKt.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ MockitoAnnotations.initMocks(this);
+
+ setupLockIconViewMocks();
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
+ Rect windowBounds = new Rect(0, 0, 800, 1200);
+ when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds);
+ when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
+ when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
+ when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING);
+ when(mAuthController.getScaleFactor()).thenReturn(1f);
+
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+
+ mUnderTest = new LockIconViewController(
+ mLockIconView,
+ mStatusBarStateController,
+ mKeyguardUpdateMonitor,
+ mKeyguardViewController,
+ mKeyguardStateController,
+ mFalsingManager,
+ mAuthController,
+ mDumpManager,
+ mAccessibilityManager,
+ mConfigurationController,
+ mDelayableExecutor,
+ mVibrator,
+ mAuthRippleController,
+ mResources,
+ new KeyguardTransitionInteractor(mTransitionRepository),
+ new KeyguardInteractor(new FakeKeyguardRepository()),
+ mFeatureFlags
+ );
+ }
+
+ @After
+ public void tearDown() {
+ mStaticMockSession.finishMocking();
+ }
+
+ protected Pair<Float, Point> setupUdfps() {
+ when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true);
+ final Point udfpsLocation = new Point(50, 75);
+ final float radius = 33f;
+ when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation);
+ when(mAuthController.getUdfpsRadius()).thenReturn(radius);
+
+ return new Pair(radius, udfpsLocation);
+ }
+
+ protected void setupShowLockIcon() {
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+ when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+ }
+
+ protected void captureAuthControllerCallback() {
+ verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
+ mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
+ }
+
+ protected void captureKeyguardStateCallback() {
+ verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
+ mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
+ }
+
+ protected void captureStatusBarStateListener() {
+ verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
+ mStatusBarStateListener = mStatusBarStateCaptor.getValue();
+ }
+
+ protected void captureKeyguardUpdateMonitorCallback() {
+ verify(mKeyguardUpdateMonitor).registerCallback(
+ mKeyguardUpdateMonitorCallbackCaptor.capture());
+ mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
+ }
+
+ protected void setupLockIconViewMocks() {
+ when(mLockIconView.getResources()).thenReturn(mResources);
+ when(mLockIconView.getContext()).thenReturn(mContext);
+ }
+
+ protected void resetLockIconView() {
+ reset(mLockIconView);
+ setupLockIconViewMocks();
+ }
+
+ protected void init(boolean useMigrationFlag) {
+ when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag);
+ mUnderTest.init();
+
+ verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture());
+ mAttachCaptor.getValue().onViewAttachedToWindow(mLockIconView);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
new file mode 100644
index 0000000..da40595
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2021 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 static com.android.keyguard.LockIconView.ICON_LOCK;
+import static com.android.keyguard.LockIconView.ICON_UNLOCK;
+
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.hardware.biometrics.BiometricSourceType;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.doze.util.BurnInHelperKt;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class LockIconViewControllerTest extends LockIconViewControllerBaseTest {
+
+ @Test
+ public void testUpdateFingerprintLocationOnInit() {
+ // GIVEN fp sensor location is available pre-attached
+ Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
+
+ // WHEN lock icon view controller is initialized and attached
+ init(/* useMigrationFlag= */false);
+
+ // THEN lock icon view location is updated to the udfps location with UDFPS radius
+ verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+ eq(PADDING));
+ }
+
+ @Test
+ public void testUpdatePaddingBasedOnResolutionScale() {
+ // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5
+ Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
+ when(mAuthController.getScaleFactor()).thenReturn(5f);
+
+ // WHEN lock icon view controller is initialized and attached
+ init(/* useMigrationFlag= */false);
+
+ // THEN lock icon view location is updated with the scaled radius
+ verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+ eq(PADDING * 5));
+ }
+
+ @Test
+ public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
+ // GIVEN fp sensor location is not available pre-init
+ when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+ when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+ init(/* useMigrationFlag= */false);
+ resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+ // GIVEN fp sensor location is available post-attached
+ captureAuthControllerCallback();
+ Pair<Float, Point> udfps = setupUdfps();
+
+ // WHEN all authenticators are registered
+ mAuthControllerCallback.onAllAuthenticatorsRegistered();
+ mDelayableExecutor.runAllReady();
+
+ // THEN lock icon view location is updated with the same coordinates as auth controller vals
+ verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+ eq(PADDING));
+ }
+
+ @Test
+ public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
+ // GIVEN fp sensor location is not available pre-init
+ when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+ when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+ init(/* useMigrationFlag= */false);
+ resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+ // GIVEN fp sensor location is available post-attached
+ captureAuthControllerCallback();
+ Pair<Float, Point> udfps = setupUdfps();
+
+ // WHEN udfps location changes
+ mAuthControllerCallback.onUdfpsLocationChanged();
+ mDelayableExecutor.runAllReady();
+
+ // THEN lock icon view location is updated with the same coordinates as auth controller vals
+ verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+ eq(PADDING));
+ }
+
+ @Test
+ public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
+ // GIVEN Udpfs sensor location is available
+ setupUdfps();
+
+ // WHEN the view is attached
+ init(/* useMigrationFlag= */false);
+
+ // THEN the lock icon view background should be enabled
+ verify(mLockIconView).setUseBackground(true);
+ }
+
+ @Test
+ public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() {
+ // GIVEN Udfps sensor location is not supported
+ when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+
+ // WHEN the view is attached
+ init(/* useMigrationFlag= */false);
+
+ // THEN the lock icon view background should be disabled
+ verify(mLockIconView).setUseBackground(false);
+ }
+
+ @Test
+ public void testUnlockIconShows_biometricUnlockedTrue() {
+ // GIVEN UDFPS sensor location is available
+ setupUdfps();
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */false);
+ captureKeyguardUpdateMonitorCallback();
+
+ // GIVEN user has unlocked with a biometric auth (ie: face auth)
+ when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
+ reset(mLockIconView);
+
+ // WHEN face auth's biometric running state changes
+ mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
+ BiometricSourceType.FACE);
+
+ // THEN the unlock icon is shown
+ verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
+ }
+
+ @Test
+ public void testLockIconStartState() {
+ // GIVEN lock icon state
+ setupShowLockIcon();
+
+ // WHEN lock icon controller is initialized
+ init(/* useMigrationFlag= */false);
+
+ // THEN the lock icon should show
+ verify(mLockIconView).updateIcon(ICON_LOCK, false);
+ }
+
+ @Test
+ public void testLockIcon_updateToUnlock() {
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon();
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */false);
+ captureKeyguardStateCallback();
+ reset(mLockIconView);
+
+ // WHEN the unlocked state changes to canDismissLockScreen=true
+ when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+ mKeyguardStateCallback.onUnlockedChanged();
+
+ // THEN the unlock should show
+ verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
+ }
+
+ @Test
+ public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
+ // GIVEN udfps not enrolled
+ setupUdfps();
+ when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
+
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon();
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */false);
+ captureStatusBarStateListener();
+ reset(mLockIconView);
+
+ // WHEN the dozing state changes
+ mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+ // THEN the icon is cleared
+ verify(mLockIconView).clearIcon();
+ }
+
+ @Test
+ public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
+ // GIVEN udfps enrolled
+ setupUdfps();
+ when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon();
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */false);
+ captureStatusBarStateListener();
+ reset(mLockIconView);
+
+ // WHEN the dozing state changes
+ mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+ // THEN the AOD lock icon should show
+ verify(mLockIconView).updateIcon(ICON_LOCK, true);
+ }
+
+ @Test
+ public void testBurnInOffsetsUpdated_onDozeAmountChanged() {
+ // GIVEN udfps enrolled
+ setupUdfps();
+ when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+ // GIVEN burn-in offset = 5
+ int burnInOffset = 5;
+ when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset);
+
+ // GIVEN starting state for the lock icon (keyguard)
+ setupShowLockIcon();
+ init(/* useMigrationFlag= */false);
+ captureStatusBarStateListener();
+ reset(mLockIconView);
+
+ // WHEN dozing updates
+ mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+ mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
+
+ // THEN the view's translation is updated to use the AoD burn-in offsets
+ verify(mLockIconView).setTranslationY(burnInOffset);
+ verify(mLockIconView).setTranslationX(burnInOffset);
+ reset(mLockIconView);
+
+ // WHEN the device is no longer dozing
+ mStatusBarStateListener.onDozingChanged(false /* isDozing */);
+ mStatusBarStateListener.onDozeAmountChanged(0f, 0f);
+
+ // THEN the view is updated to NO translation (no burn-in offsets anymore)
+ verify(mLockIconView).setTranslationY(0);
+ verify(mLockIconView).setTranslationX(0);
+
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt
new file mode 100644
index 0000000..d2c54b4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.keyguard.LockIconView.ICON_LOCK
+import com.android.systemui.doze.util.getBurnInOffset
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LockIconViewControllerWithCoroutinesTest : LockIconViewControllerBaseTest() {
+
+ /** After migration, replaces LockIconViewControllerTest version */
+ @Test
+ fun testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() =
+ runBlocking(IMMEDIATE) {
+ // GIVEN udfps not enrolled
+ setupUdfps()
+ whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false)
+
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon()
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */ true)
+ reset(mLockIconView)
+
+ // WHEN the dozing state changes
+ mUnderTest.mIsDozingCallback.accept(true)
+
+ // THEN the icon is cleared
+ verify(mLockIconView).clearIcon()
+ }
+
+ /** After migration, replaces LockIconViewControllerTest version */
+ @Test
+ fun testLockIcon_updateToAodLock_whenUdfpsEnrolled() =
+ runBlocking(IMMEDIATE) {
+ // GIVEN udfps enrolled
+ setupUdfps()
+ whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true)
+
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon()
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */ true)
+ reset(mLockIconView)
+
+ // WHEN the dozing state changes
+ mUnderTest.mIsDozingCallback.accept(true)
+
+ // THEN the AOD lock icon should show
+ verify(mLockIconView).updateIcon(ICON_LOCK, true)
+ }
+
+ /** After migration, replaces LockIconViewControllerTest version */
+ @Test
+ fun testBurnInOffsetsUpdated_onDozeAmountChanged() =
+ runBlocking(IMMEDIATE) {
+ // GIVEN udfps enrolled
+ setupUdfps()
+ whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true)
+
+ // GIVEN burn-in offset = 5
+ val burnInOffset = 5
+ whenever(getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset)
+
+ // GIVEN starting state for the lock icon (keyguard)
+ setupShowLockIcon()
+ init(/* useMigrationFlag= */ true)
+ reset(mLockIconView)
+
+ // WHEN dozing updates
+ mUnderTest.mIsDozingCallback.accept(true)
+ mUnderTest.mDozeTransitionCallback.accept(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+
+ // THEN the view's translation is updated to use the AoD burn-in offsets
+ verify(mLockIconView).setTranslationY(burnInOffset.toFloat())
+ verify(mLockIconView).setTranslationX(burnInOffset.toFloat())
+ reset(mLockIconView)
+
+ // WHEN the device is no longer dozing
+ mUnderTest.mIsDozingCallback.accept(false)
+ mUnderTest.mDozeTransitionCallback.accept(TransitionStep(AOD, LOCKSCREEN, 0f, FINISHED))
+
+ // THEN the view is updated to NO translation (no burn-in offsets anymore)
+ verify(mLockIconView).setTranslationY(0f)
+ verify(mLockIconView).setTranslationX(0f)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index d52612b..e8c760c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -52,6 +52,7 @@
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@@ -123,6 +124,21 @@
}
@Test
+ fun testDismissBeforeIntroEnd() {
+ val container = initializeFingerprintContainer()
+ waitForIdleSync()
+
+ // STATE_ANIMATING_IN = 1
+ container?.mContainerState = 1
+
+ container.dismissWithoutCallback(false)
+
+ // the first time is triggered by initializeFingerprintContainer()
+ // the second time was triggered by dismissWithoutCallback()
+ verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+ }
+
+ @Test
fun testDismissesOnFocusLoss() {
val container = initializeFingerprintContainer()
waitForIdleSync()
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 d610709..28e13b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -169,6 +169,8 @@
@Mock
private LatencyTracker mLatencyTracker;
private FakeExecutor mFgExecutor;
+ @Mock
+ private UdfpsDisplayMode mUdfpsDisplayMode;
// Stuff for configuring mocks
@Mock
@@ -258,7 +260,6 @@
mVibrator,
mUdfpsHapticsSimulator,
mUdfpsShell,
- Optional.of(mDisplayModeProvider),
mKeyguardStateController,
mDisplayManager,
mHandler,
@@ -275,6 +276,7 @@
verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
mScreenObserver = mScreenObserverCaptor.getValue();
mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, new UdfpsOverlayParams());
+ mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java
new file mode 100644
index 0000000..7e35b26
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.biometrics;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper.RunWithLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.concurrency.FakeExecution;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper(setAsMainLooper = true)
+public class UdfpsDisplayModeTest extends SysuiTestCase {
+ private static final int DISPLAY_ID = 0;
+
+ @Mock
+ private AuthController mAuthController;
+ @Mock
+ private IUdfpsHbmListener mDisplayCallback;
+ @Mock
+ private Runnable mOnEnabled;
+ @Mock
+ private Runnable mOnDisabled;
+
+ private final FakeExecution mExecution = new FakeExecution();
+ private UdfpsDisplayMode mHbmController;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ // Force mContext to always return DISPLAY_ID
+ Context contextSpy = spy(mContext);
+ when(contextSpy.getDisplayId()).thenReturn(DISPLAY_ID);
+
+ // Set up mocks.
+ when(mAuthController.getUdfpsHbmListener()).thenReturn(mDisplayCallback);
+
+ // Create a real controller with mock dependencies.
+ mHbmController = new UdfpsDisplayMode(contextSpy, mExecution, mAuthController);
+ }
+
+ @Test
+ public void roundTrip() throws RemoteException {
+ // Enable the UDFPS mode.
+ mHbmController.enable(mOnEnabled);
+
+ // Should set the appropriate refresh rate for UDFPS and notify the caller.
+ verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+ verify(mOnEnabled).run();
+
+ // Disable the UDFPS mode.
+ mHbmController.disable(mOnDisabled);
+
+ // Should unset the refresh rate and notify the caller.
+ verify(mOnDisabled).run();
+ verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID));
+ }
+
+ @Test
+ public void mustNotEnableMoreThanOnce() throws RemoteException {
+ // First request to enable the UDFPS mode.
+ mHbmController.enable(mOnEnabled);
+
+ // Should set the appropriate refresh rate for UDFPS and notify the caller.
+ verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+ verify(mOnEnabled).run();
+
+ // Second request to enable the UDFPS mode, while it's still enabled.
+ mHbmController.enable(mOnEnabled);
+
+ // Should ignore the second request.
+ verifyNoMoreInteractions(mDisplayCallback);
+ verifyNoMoreInteractions(mOnEnabled);
+ }
+
+ @Test
+ public void mustNotDisableMoreThanOnce() throws RemoteException {
+ // Disable the UDFPS mode.
+ mHbmController.enable(mOnEnabled);
+
+ // Should set the appropriate refresh rate for UDFPS and notify the caller.
+ verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+ verify(mOnEnabled).run();
+
+ // First request to disable the UDFPS mode.
+ mHbmController.disable(mOnDisabled);
+
+ // Should unset the refresh rate and notify the caller.
+ verify(mOnDisabled).run();
+ verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID));
+
+ // Second request to disable the UDFPS mode, when it's already disabled.
+ mHbmController.disable(mOnDisabled);
+
+ // Should ignore the second request.
+ verifyNoMoreInteractions(mOnDisabled);
+ verifyNoMoreInteractions(mDisplayCallback);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index 677c7bd..b7f1c1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -39,8 +39,8 @@
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.screenshot.TimeoutHandler;
+import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -48,7 +48,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-@Ignore("b/254635291")
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ClipboardOverlayControllerTest extends SysuiTestCase {
@@ -97,6 +96,11 @@
mCallbacks = mOverlayCallbacksCaptor.getValue();
}
+ @After
+ public void tearDown() {
+ mOverlayController.hideImmediate();
+ }
+
@Test
public void test_setClipData_nullData() {
ClipData clipData = null;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
deleted file mode 100644
index cefd68d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ /dev/null
@@ -1,476 +0,0 @@
-/*
- * Copyright (C) 2021 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.keyguard;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.keyguard.LockIconView.ICON_LOCK;
-import static com.android.keyguard.LockIconView.ICON_UNLOCK;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.AnimatedStateListDrawable;
-import android.hardware.biometrics.BiometricSourceType;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.util.Pair;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.KeyguardViewController;
-import com.android.keyguard.LockIconView;
-import com.android.keyguard.LockIconViewController;
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.biometrics.AuthRippleController;
-import com.android.systemui.doze.util.BurnInHelperKt;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class LockIconViewControllerTest extends SysuiTestCase {
- private static final String UNLOCKED_LABEL = "unlocked";
- private static final int PADDING = 10;
-
- private MockitoSession mStaticMockSession;
-
- private @Mock LockIconView mLockIconView;
- private @Mock AnimatedStateListDrawable mIconDrawable;
- private @Mock Context mContext;
- private @Mock Resources mResources;
- private @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager;
- private @Mock StatusBarStateController mStatusBarStateController;
- private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- private @Mock KeyguardViewController mKeyguardViewController;
- private @Mock KeyguardStateController mKeyguardStateController;
- private @Mock FalsingManager mFalsingManager;
- private @Mock AuthController mAuthController;
- private @Mock DumpManager mDumpManager;
- private @Mock AccessibilityManager mAccessibilityManager;
- private @Mock ConfigurationController mConfigurationController;
- private @Mock VibratorHelper mVibrator;
- private @Mock AuthRippleController mAuthRippleController;
- private FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
-
- private LockIconViewController mLockIconViewController;
-
- // Capture listeners so that they can be used to send events
- @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
- ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
- private View.OnAttachStateChangeListener mAttachListener;
-
- @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
- ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
- private KeyguardStateController.Callback mKeyguardStateCallback;
-
- @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
- ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
- private StatusBarStateController.StateListener mStatusBarStateListener;
-
- @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
- private AuthController.Callback mAuthControllerCallback;
-
- @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback>
- mKeyguardUpdateMonitorCallbackCaptor =
- ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
- private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
-
- @Captor private ArgumentCaptor<Point> mPointCaptor;
-
- @Before
- public void setUp() throws Exception {
- mStaticMockSession = mockitoSession()
- .mockStatic(BurnInHelperKt.class)
- .strictness(Strictness.LENIENT)
- .startMocking();
- MockitoAnnotations.initMocks(this);
-
- setupLockIconViewMocks();
- when(mContext.getResources()).thenReturn(mResources);
- when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
- Rect windowBounds = new Rect(0, 0, 800, 1200);
- when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds);
- when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
- when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
- when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING);
- when(mAuthController.getScaleFactor()).thenReturn(1f);
-
- when(mKeyguardStateController.isShowing()).thenReturn(true);
- when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
- when(mStatusBarStateController.isDozing()).thenReturn(false);
- when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
-
- mLockIconViewController = new LockIconViewController(
- mLockIconView,
- mStatusBarStateController,
- mKeyguardUpdateMonitor,
- mKeyguardViewController,
- mKeyguardStateController,
- mFalsingManager,
- mAuthController,
- mDumpManager,
- mAccessibilityManager,
- mConfigurationController,
- mDelayableExecutor,
- mVibrator,
- mAuthRippleController,
- mResources
- );
- }
-
- @After
- public void tearDown() {
- mStaticMockSession.finishMocking();
- }
-
- @Test
- public void testUpdateFingerprintLocationOnInit() {
- // GIVEN fp sensor location is available pre-attached
- Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
-
- // WHEN lock icon view controller is initialized and attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN lock icon view location is updated to the udfps location with UDFPS radius
- verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
- eq(PADDING));
- }
-
- @Test
- public void testUpdatePaddingBasedOnResolutionScale() {
- // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5
- Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
- when(mAuthController.getScaleFactor()).thenReturn(5f);
-
- // WHEN lock icon view controller is initialized and attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN lock icon view location is updated with the scaled radius
- verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
- eq(PADDING * 5));
- }
-
- @Test
- public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
- // GIVEN fp sensor location is not available pre-init
- when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
- when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- resetLockIconView(); // reset any method call counts for when we verify method calls later
-
- // GIVEN fp sensor location is available post-attached
- captureAuthControllerCallback();
- Pair<Float, Point> udfps = setupUdfps();
-
- // WHEN all authenticators are registered
- mAuthControllerCallback.onAllAuthenticatorsRegistered();
- mDelayableExecutor.runAllReady();
-
- // THEN lock icon view location is updated with the same coordinates as auth controller vals
- verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
- eq(PADDING));
- }
-
- @Test
- public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
- // GIVEN fp sensor location is not available pre-init
- when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
- when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- resetLockIconView(); // reset any method call counts for when we verify method calls later
-
- // GIVEN fp sensor location is available post-attached
- captureAuthControllerCallback();
- Pair<Float, Point> udfps = setupUdfps();
-
- // WHEN udfps location changes
- mAuthControllerCallback.onUdfpsLocationChanged();
- mDelayableExecutor.runAllReady();
-
- // THEN lock icon view location is updated with the same coordinates as auth controller vals
- verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
- eq(PADDING));
- }
-
- @Test
- public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
- // GIVEN Udpfs sensor location is available
- setupUdfps();
-
- mLockIconViewController.init();
- captureAttachListener();
-
- // WHEN the view is attached
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN the lock icon view background should be enabled
- verify(mLockIconView).setUseBackground(true);
- }
-
- @Test
- public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() {
- // GIVEN Udfps sensor location is not supported
- when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
-
- mLockIconViewController.init();
- captureAttachListener();
-
- // WHEN the view is attached
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN the lock icon view background should be disabled
- verify(mLockIconView).setUseBackground(false);
- }
-
- @Test
- public void testUnlockIconShows_biometricUnlockedTrue() {
- // GIVEN UDFPS sensor location is available
- setupUdfps();
-
- // GIVEN lock icon controller is initialized and view is attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureKeyguardUpdateMonitorCallback();
-
- // GIVEN user has unlocked with a biometric auth (ie: face auth)
- when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
- reset(mLockIconView);
-
- // WHEN face auth's biometric running state changes
- mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
- BiometricSourceType.FACE);
-
- // THEN the unlock icon is shown
- verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
- }
-
- @Test
- public void testLockIconStartState() {
- // GIVEN lock icon state
- setupShowLockIcon();
-
- // WHEN lock icon controller is initialized
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN the lock icon should show
- verify(mLockIconView).updateIcon(ICON_LOCK, false);
- }
-
- @Test
- public void testLockIcon_updateToUnlock() {
- // GIVEN starting state for the lock icon
- setupShowLockIcon();
-
- // GIVEN lock icon controller is initialized and view is attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureKeyguardStateCallback();
- reset(mLockIconView);
-
- // WHEN the unlocked state changes to canDismissLockScreen=true
- when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
- mKeyguardStateCallback.onUnlockedChanged();
-
- // THEN the unlock should show
- verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
- }
-
- @Test
- public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
- // GIVEN udfps not enrolled
- setupUdfps();
- when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
-
- // GIVEN starting state for the lock icon
- setupShowLockIcon();
-
- // GIVEN lock icon controller is initialized and view is attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureStatusBarStateListener();
- reset(mLockIconView);
-
- // WHEN the dozing state changes
- mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-
- // THEN the icon is cleared
- verify(mLockIconView).clearIcon();
- }
-
- @Test
- public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
- // GIVEN udfps enrolled
- setupUdfps();
- when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
-
- // GIVEN starting state for the lock icon
- setupShowLockIcon();
-
- // GIVEN lock icon controller is initialized and view is attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureStatusBarStateListener();
- reset(mLockIconView);
-
- // WHEN the dozing state changes
- mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-
- // THEN the AOD lock icon should show
- verify(mLockIconView).updateIcon(ICON_LOCK, true);
- }
-
- @Test
- public void testBurnInOffsetsUpdated_onDozeAmountChanged() {
- // GIVEN udfps enrolled
- setupUdfps();
- when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
-
- // GIVEN burn-in offset = 5
- int burnInOffset = 5;
- when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset);
-
- // GIVEN starting state for the lock icon (keyguard)
- setupShowLockIcon();
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureStatusBarStateListener();
- reset(mLockIconView);
-
- // WHEN dozing updates
- mStatusBarStateListener.onDozingChanged(true /* isDozing */);
- mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
-
- // THEN the view's translation is updated to use the AoD burn-in offsets
- verify(mLockIconView).setTranslationY(burnInOffset);
- verify(mLockIconView).setTranslationX(burnInOffset);
- reset(mLockIconView);
-
- // WHEN the device is no longer dozing
- mStatusBarStateListener.onDozingChanged(false /* isDozing */);
- mStatusBarStateListener.onDozeAmountChanged(0f, 0f);
-
- // THEN the view is updated to NO translation (no burn-in offsets anymore)
- verify(mLockIconView).setTranslationY(0);
- verify(mLockIconView).setTranslationX(0);
-
- }
- private Pair<Float, Point> setupUdfps() {
- when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true);
- final Point udfpsLocation = new Point(50, 75);
- final float radius = 33f;
- when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation);
- when(mAuthController.getUdfpsRadius()).thenReturn(radius);
-
- return new Pair(radius, udfpsLocation);
- }
-
- private void setupShowLockIcon() {
- when(mKeyguardStateController.isShowing()).thenReturn(true);
- when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
- when(mStatusBarStateController.isDozing()).thenReturn(false);
- when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
- when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
- when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
- }
-
- private void captureAuthControllerCallback() {
- verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
- mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
- }
-
- private void captureAttachListener() {
- verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture());
- mAttachListener = mAttachCaptor.getValue();
- }
-
- private void captureKeyguardStateCallback() {
- verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
- mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
- }
-
- private void captureStatusBarStateListener() {
- verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
- mStatusBarStateListener = mStatusBarStateCaptor.getValue();
- }
-
- private void captureKeyguardUpdateMonitorCallback() {
- verify(mKeyguardUpdateMonitor).registerCallback(
- mKeyguardUpdateMonitorCallbackCaptor.capture());
- mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
- }
-
- private void setupLockIconViewMocks() {
- when(mLockIconView.getResources()).thenReturn(mResources);
- when(mLockIconView.getContext()).thenReturn(mContext);
- }
-
- private void resetLockIconView() {
- reset(mLockIconView);
- setupLockIconViewMocks();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
new file mode 100644
index 0000000..1130bda
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.settings.brightness
+
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.intercepting.SingleActivityFactory
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class BrightnessDialogTest : SysuiTestCase() {
+
+ @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory
+ @Mock private lateinit var backgroundHandler: Handler
+ @Mock private lateinit var brightnessSliderController: BrightnessSliderController
+
+ @Rule
+ @JvmField
+ var activityRule =
+ ActivityTestRule(
+ object : SingleActivityFactory<TestDialog>(TestDialog::class.java) {
+ override fun create(intent: Intent?): TestDialog {
+ return TestDialog(
+ fakeBroadcastDispatcher,
+ brightnessSliderControllerFactory,
+ backgroundHandler
+ )
+ }
+ },
+ false,
+ false
+ )
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ `when`(brightnessSliderControllerFactory.create(any(), any()))
+ .thenReturn(brightnessSliderController)
+ `when`(brightnessSliderController.rootView).thenReturn(View(context))
+
+ activityRule.launchActivity(null)
+ }
+
+ @After
+ fun tearDown() {
+ activityRule.finishActivity()
+ }
+
+ @Test
+ fun testGestureExclusion() {
+ val frame = activityRule.activity.requireViewById<View>(R.id.brightness_mirror_container)
+
+ val lp = frame.layoutParams as ViewGroup.MarginLayoutParams
+ val horizontalMargin =
+ activityRule.activity.resources.getDimensionPixelSize(
+ R.dimen.notification_side_paddings
+ )
+ assertThat(lp.leftMargin).isEqualTo(horizontalMargin)
+ assertThat(lp.rightMargin).isEqualTo(horizontalMargin)
+
+ assertThat(frame.systemGestureExclusionRects.size).isEqualTo(1)
+ val exclusion = frame.systemGestureExclusionRects[0]
+ assertThat(exclusion)
+ .isEqualTo(Rect(-horizontalMargin, 0, frame.width + horizontalMargin, frame.height))
+ }
+
+ class TestDialog(
+ broadcastDispatcher: BroadcastDispatcher,
+ brightnessSliderControllerFactory: BrightnessSliderController.Factory,
+ backgroundHandler: Handler
+ ) : BrightnessDialog(broadcastDispatcher, brightnessSliderControllerFactory, backgroundHandler)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index cf5fa87..64dc956 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -16,6 +16,11 @@
package com.android.systemui.shared.system;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -25,11 +30,6 @@
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_STANDARD;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CHANGING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -40,6 +40,7 @@
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
@@ -73,12 +74,12 @@
.addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */)
.addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build();
// Check apps extraction
- RemoteAnimationTargetCompat[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
+ RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
mock(SurfaceControl.Transaction.class), null /* leashes */);
assertEquals(2, wrapped.length);
int changeLayer = -1;
int closeLayer = -1;
- for (RemoteAnimationTargetCompat t : wrapped) {
+ for (RemoteAnimationTarget t : wrapped) {
if (t.mode == MODE_CHANGING) {
changeLayer = t.prefixOrderIndex;
} else if (t.mode == MODE_CLOSING) {
@@ -91,14 +92,14 @@
assertTrue(closeLayer < changeLayer);
// Check wallpaper extraction
- RemoteAnimationTargetCompat[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+ RemoteAnimationTarget[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
true /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
assertEquals(1, wallps.length);
assertTrue(wallps[0].prefixOrderIndex < closeLayer);
assertEquals(MODE_OPENING, wallps[0].mode);
// Check non-apps extraction
- RemoteAnimationTargetCompat[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+ RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
assertEquals(1, nonApps.length);
assertTrue(nonApps[0].prefixOrderIndex < closeLayer);
@@ -115,9 +116,9 @@
change.setTaskInfo(createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_HOME));
change.setEndAbsBounds(endBounds);
change.setEndRelOffset(0, 0);
- final RemoteAnimationTargetCompat wrapped = new RemoteAnimationTargetCompat(change,
- 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class));
- assertEquals(ACTIVITY_TYPE_HOME, wrapped.activityType);
+ RemoteAnimationTarget wrapped = RemoteAnimationTargetCompat.newTarget(
+ change, 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class), null);
+ assertEquals(ACTIVITY_TYPE_HOME, wrapped.windowConfiguration.getActivityType());
assertEquals(new Rect(0, 0, 100, 140), wrapped.localBounds);
assertEquals(endBounds, wrapped.screenSpaceBounds);
assertTrue(wrapped.isTranslucent);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
deleted file mode 100644
index 81b8e98..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.notification.row;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.view.NotificationHeaderView;
-import android.view.View;
-import android.view.ViewPropertyAnimator;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.R;
-import com.android.internal.widget.NotificationActionListLayout;
-import com.android.internal.widget.NotificationExpandButton;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.statusbar.notification.FeedbackIcon;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class NotificationContentViewTest extends SysuiTestCase {
-
- NotificationContentView mView;
-
- @Before
- @UiThreadTest
- public void setup() {
- mDependency.injectMockDependency(MediaOutputDialogFactory.class);
-
- mView = new NotificationContentView(mContext, null);
- ExpandableNotificationRow row = new ExpandableNotificationRow(mContext, null);
- ExpandableNotificationRow mockRow = spy(row);
- doReturn(10).when(mockRow).getIntrinsicHeight();
-
- mView.setContainingNotification(mockRow);
- mView.setHeights(10, 20, 30);
-
- mView.setContractedChild(createViewWithHeight(10));
- mView.setExpandedChild(createViewWithHeight(20));
- mView.setHeadsUpChild(createViewWithHeight(30));
-
- mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
- mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
- }
-
- private View createViewWithHeight(int height) {
- View view = new View(mContext, null);
- view.setMinimumHeight(height);
- return view;
- }
-
- @Test
- @UiThreadTest
- public void testSetFeedbackIcon() {
- View mockContracted = mock(NotificationHeaderView.class);
- when(mockContracted.findViewById(com.android.internal.R.id.feedback))
- .thenReturn(mockContracted);
- when(mockContracted.getContext()).thenReturn(mContext);
- View mockExpanded = mock(NotificationHeaderView.class);
- when(mockExpanded.findViewById(com.android.internal.R.id.feedback))
- .thenReturn(mockExpanded);
- when(mockExpanded.getContext()).thenReturn(mContext);
- View mockHeadsUp = mock(NotificationHeaderView.class);
- when(mockHeadsUp.findViewById(com.android.internal.R.id.feedback))
- .thenReturn(mockHeadsUp);
- when(mockHeadsUp.getContext()).thenReturn(mContext);
-
- mView.setContractedChild(mockContracted);
- mView.setExpandedChild(mockExpanded);
- mView.setHeadsUpChild(mockHeadsUp);
-
- mView.setFeedbackIcon(new FeedbackIcon(R.drawable.ic_feedback_alerted,
- R.string.notification_feedback_indicator_alerted));
-
- verify(mockContracted, times(1)).setVisibility(View.VISIBLE);
- verify(mockExpanded, times(1)).setVisibility(View.VISIBLE);
- verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE);
- }
-
- @Test
- @UiThreadTest
- public void testExpandButtonFocusIsCalled() {
- View mockContractedEB = mock(NotificationExpandButton.class);
- View mockContracted = mock(NotificationHeaderView.class);
- when(mockContracted.animate()).thenReturn(mock(ViewPropertyAnimator.class));
- when(mockContracted.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
- mockContractedEB);
- when(mockContracted.getContext()).thenReturn(mContext);
-
- View mockExpandedEB = mock(NotificationExpandButton.class);
- View mockExpanded = mock(NotificationHeaderView.class);
- when(mockExpanded.animate()).thenReturn(mock(ViewPropertyAnimator.class));
- when(mockExpanded.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
- mockExpandedEB);
- when(mockExpanded.getContext()).thenReturn(mContext);
-
- View mockHeadsUpEB = mock(NotificationExpandButton.class);
- View mockHeadsUp = mock(NotificationHeaderView.class);
- when(mockHeadsUp.animate()).thenReturn(mock(ViewPropertyAnimator.class));
- when(mockHeadsUp.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
- mockHeadsUpEB);
- when(mockHeadsUp.getContext()).thenReturn(mContext);
-
- // Set up all 3 child forms
- mView.setContractedChild(mockContracted);
- mView.setExpandedChild(mockExpanded);
- mView.setHeadsUpChild(mockHeadsUp);
-
- // This is required to call requestAccessibilityFocus()
- mView.setFocusOnVisibilityChange();
-
- // The following will initialize the view and switch from not visible to expanded.
- // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
- mView.setHeadsUp(true);
-
- verify(mockContractedEB, times(0)).requestAccessibilityFocus();
- verify(mockExpandedEB, times(1)).requestAccessibilityFocus();
- verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus();
- }
-
- @Test
- @UiThreadTest
- public void testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
- View mockContracted = mock(NotificationHeaderView.class);
-
- View mockExpandedActions = mock(NotificationActionListLayout.class);
- View mockExpanded = mock(NotificationHeaderView.class);
- when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
- mockExpandedActions);
-
- View mockHeadsUpActions = mock(NotificationActionListLayout.class);
- View mockHeadsUp = mock(NotificationHeaderView.class);
- when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
- mockHeadsUpActions);
-
- mView.setContractedChild(mockContracted);
- mView.setExpandedChild(mockExpanded);
- mView.setHeadsUpChild(mockHeadsUp);
-
- mView.setRemoteInputVisible(true);
-
- verify(mockContracted, times(0)).findViewById(0);
- verify(mockExpandedActions, times(1)).setImportantForAccessibility(
- View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
- View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- }
-
- @Test
- @UiThreadTest
- public void testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
- View mockContracted = mock(NotificationHeaderView.class);
-
- View mockExpandedActions = mock(NotificationActionListLayout.class);
- View mockExpanded = mock(NotificationHeaderView.class);
- when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
- mockExpandedActions);
-
- View mockHeadsUpActions = mock(NotificationActionListLayout.class);
- View mockHeadsUp = mock(NotificationHeaderView.class);
- when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
- mockHeadsUpActions);
-
- mView.setContractedChild(mockContracted);
- mView.setExpandedChild(mockExpanded);
- mView.setHeadsUpChild(mockHeadsUp);
-
- mView.setRemoteInputVisible(false);
-
- verify(mockContracted, times(0)).findViewById(0);
- verify(mockExpandedActions, times(1)).setImportantForAccessibility(
- View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
- View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
new file mode 100644
index 0000000..562b4df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -0,0 +1,350 @@
+/*
+ * 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.statusbar.notification.row
+
+import android.content.res.Resources
+import android.os.UserHandle
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import android.view.NotificationHeaderView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.widget.NotificationActionListLayout
+import com.android.internal.widget.NotificationExpandButton
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.statusbar.notification.FeedbackIcon
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationContentViewTest : SysuiTestCase() {
+ private lateinit var view: NotificationContentView
+
+ @Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier
+
+ private val notificationContentMargin =
+ mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin)
+
+ @Before
+ fun setup() {
+ initMocks(this)
+
+ mDependency.injectMockDependency(MediaOutputDialogFactory::class.java)
+
+ view = spy(NotificationContentView(mContext, /* attrs= */ null))
+ val row = ExpandableNotificationRow(mContext, /* attrs= */ null)
+ row.entry = createMockNotificationEntry(false)
+ val spyRow = spy(row)
+ doReturn(10).whenever(spyRow).intrinsicHeight
+
+ with(view) {
+ initialize(mPeopleNotificationIdentifier, mock(), mock(), mock())
+ setContainingNotification(spyRow)
+ setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30)
+ contractedChild = createViewWithHeight(10)
+ expandedChild = createViewWithHeight(20)
+ headsUpChild = createViewWithHeight(30)
+ measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
+ layout(0, 0, view.measuredWidth, view.measuredHeight)
+ }
+ }
+
+ private fun createViewWithHeight(height: Int) =
+ View(mContext, /* attrs= */ null).apply { minimumHeight = height }
+
+ @Test
+ fun testSetFeedbackIcon() {
+ // Given: contractedChild, enpandedChild, and headsUpChild being set
+ val mockContracted = createMockNotificationHeaderView()
+ val mockExpanded = createMockNotificationHeaderView()
+ val mockHeadsUp = createMockNotificationHeaderView()
+
+ with(view) {
+ contractedChild = mockContracted
+ expandedChild = mockExpanded
+ headsUpChild = mockHeadsUp
+ }
+
+ // When: FeedBackIcon is set
+ view.setFeedbackIcon(
+ FeedbackIcon(
+ R.drawable.ic_feedback_alerted,
+ R.string.notification_feedback_indicator_alerted
+ )
+ )
+
+ // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible
+ verify(mockContracted).visibility = View.VISIBLE
+ verify(mockExpanded).visibility = View.VISIBLE
+ verify(mockHeadsUp).visibility = View.VISIBLE
+ }
+
+ private fun createMockNotificationHeaderView() =
+ mock<NotificationHeaderView>().apply {
+ whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this)
+ whenever(this.context).thenReturn(mContext)
+ }
+
+ @Test
+ fun testExpandButtonFocusIsCalled() {
+ val mockContractedEB = mock<NotificationExpandButton>()
+ val mockContracted = createMockNotificationHeaderView(mockContractedEB)
+
+ val mockExpandedEB = mock<NotificationExpandButton>()
+ val mockExpanded = createMockNotificationHeaderView(mockExpandedEB)
+
+ val mockHeadsUpEB = mock<NotificationExpandButton>()
+ val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB)
+
+ // Set up all 3 child forms
+ view.contractedChild = mockContracted
+ view.expandedChild = mockExpanded
+ view.headsUpChild = mockHeadsUp
+
+ // This is required to call requestAccessibilityFocus()
+ view.setFocusOnVisibilityChange()
+
+ // The following will initialize the view and switch from not visible to expanded.
+ // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
+ view.setHeadsUp(true)
+ verify(mockContractedEB, never()).requestAccessibilityFocus()
+ verify(mockExpandedEB).requestAccessibilityFocus()
+ verify(mockHeadsUpEB, never()).requestAccessibilityFocus()
+ }
+
+ private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) =
+ mock<NotificationHeaderView>().apply {
+ whenever(this.animate()).thenReturn(mock())
+ whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
+ whenever(this.context).thenReturn(mContext)
+ }
+
+ @Test
+ fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
+ val mockContracted = mock<NotificationHeaderView>()
+
+ val mockExpandedActions = mock<NotificationActionListLayout>()
+ val mockExpanded = mock<NotificationHeaderView>()
+ whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
+
+ val mockHeadsUpActions = mock<NotificationActionListLayout>()
+ val mockHeadsUp = mock<NotificationHeaderView>()
+ whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
+
+ with(view) {
+ contractedChild = mockContracted
+ expandedChild = mockExpanded
+ headsUpChild = mockHeadsUp
+ }
+
+ view.setRemoteInputVisible(true)
+
+ verify(mockContracted, never()).findViewById<View>(0)
+ verify(mockExpandedActions).importantForAccessibility =
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ verify(mockHeadsUpActions).importantForAccessibility =
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ }
+
+ @Test
+ fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
+ val mockContracted = mock<NotificationHeaderView>()
+
+ val mockExpandedActions = mock<NotificationActionListLayout>()
+ val mockExpanded = mock<NotificationHeaderView>()
+ whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
+
+ val mockHeadsUpActions = mock<NotificationActionListLayout>()
+ val mockHeadsUp = mock<NotificationHeaderView>()
+ whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
+
+ with(view) {
+ contractedChild = mockContracted
+ expandedChild = mockExpanded
+ headsUpChild = mockHeadsUp
+ }
+
+ view.setRemoteInputVisible(false)
+
+ verify(mockContracted, never()).findViewById<View>(0)
+ verify(mockExpandedActions).importantForAccessibility =
+ View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ verify(mockHeadsUpActions).importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ }
+
+ @Test
+ fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
+ // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+ // Bubble button should not be shown for the given NotificationEntry
+ val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+ val actionListMarginTarget =
+ spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+ val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+ whenever(
+ mockExpandedChild.findViewById<LinearLayout>(
+ R.id.notification_action_list_margin_target
+ )
+ )
+ .thenReturn(actionListMarginTarget)
+ view.setContainingNotification(mockContainingNotification)
+
+ // When: call NotificationContentView.setExpandedChild() to set the expandedChild
+ view.expandedChild = mockExpandedChild
+
+ // Then: bottom margin of actionListMarginTarget should not change,
+ // still be notificationContentMargin
+ assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+ }
+
+ @Test
+ fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
+ // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+ // Bubble button should be shown for the given NotificationEntry
+ val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true)
+ val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+ val actionListMarginTarget =
+ spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+ val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+ whenever(
+ mockExpandedChild.findViewById<LinearLayout>(
+ R.id.notification_action_list_margin_target
+ )
+ )
+ .thenReturn(actionListMarginTarget)
+ view.setContainingNotification(mockContainingNotification)
+
+ // When: call NotificationContentView.setExpandedChild() to set the expandedChild
+ view.expandedChild = mockExpandedChild
+
+ // Then: bottom margin of actionListMarginTarget should be set to 0
+ assertEquals(0, getMarginBottom(actionListMarginTarget))
+ }
+
+ @Test
+ fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
+ // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+ val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+ val actionListMarginTarget =
+ spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+ val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+ whenever(
+ mockExpandedChild.findViewById<LinearLayout>(
+ R.id.notification_action_list_margin_target
+ )
+ )
+ .thenReturn(actionListMarginTarget)
+ view.setContainingNotification(mockContainingNotification)
+ view.expandedChild = mockExpandedChild
+ assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+
+ // When: call NotificationContentView.onNotificationUpdated() to update the
+ // NotificationEntry, which should not show bubble button
+ view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false))
+
+ // Then: bottom margin of actionListMarginTarget should not change, still be 20
+ assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+ }
+
+ @Test
+ fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
+ // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+ val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+ val actionListMarginTarget =
+ spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+ val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+ whenever(
+ mockExpandedChild.findViewById<LinearLayout>(
+ R.id.notification_action_list_margin_target
+ )
+ )
+ .thenReturn(actionListMarginTarget)
+ view.setContainingNotification(mockContainingNotification)
+ view.expandedChild = mockExpandedChild
+ assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+
+ // When: call NotificationContentView.onNotificationUpdated() to update the
+ // NotificationEntry, which should show bubble button
+ view.onNotificationUpdated(createMockNotificationEntry(true))
+
+ // Then: bottom margin of actionListMarginTarget should not change, still be 20
+ assertEquals(0, getMarginBottom(actionListMarginTarget))
+ }
+
+ private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
+ mock<ExpandableNotificationRow>().apply {
+ whenever(this.entry).thenReturn(notificationEntry)
+ whenever(this.context).thenReturn(mContext)
+ whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {})
+ }
+
+ private fun createMockNotificationEntry(showButton: Boolean) =
+ mock<NotificationEntry>().apply {
+ whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this))
+ .thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON)
+ whenever(this.bubbleMetadata).thenReturn(mock())
+ val sbnMock: StatusBarNotification = mock()
+ val userMock: UserHandle = mock()
+ whenever(this.sbn).thenReturn(sbnMock)
+ whenever(sbnMock.user).thenReturn(userMock)
+ doReturn(showButton).whenever(view).shouldShowBubbleButton(this)
+ }
+
+ private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout {
+ val outerLayout = LinearLayout(mContext)
+ val innerLayout = LinearLayout(mContext)
+ outerLayout.addView(innerLayout)
+ val mlp = innerLayout.layoutParams as ViewGroup.MarginLayoutParams
+ mlp.setMargins(0, 0, 0, bottomMargin)
+ return innerLayout
+ }
+
+ private fun createMockExpandedChild(notificationEntry: NotificationEntry) =
+ mock<ExpandableNotificationRow>().apply {
+ whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock())
+ whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock())
+ whenever(this.entry).thenReturn(notificationEntry)
+ whenever(this.context).thenReturn(mContext)
+
+ val resourcesMock: Resources = mock()
+ whenever(resourcesMock.configuration).thenReturn(mock())
+ whenever(this.resources).thenReturn(resourcesMock)
+ }
+
+ private fun getMarginBottom(layout: LinearLayout): Int =
+ (layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index a95a49c..8c8b644 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
@@ -147,8 +147,8 @@
createSection(mFirst, mSecond),
createSection(null, null)
});
- Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f);
}
@Test
@@ -170,13 +170,13 @@
when(testHelper.getStatusBarStateController().isDozing()).thenReturn(true);
row.setHeadsUp(true);
mRoundnessManager.updateView(entry.getRow(), false);
- Assert.assertEquals(1f, row.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(1f, row.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(1f, row.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(1f, row.getTopRoundness(), 0.0f);
row.setHeadsUp(false);
mRoundnessManager.updateView(entry.getRow(), false);
- Assert.assertEquals(mSmallRadiusRatio, row.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(mSmallRadiusRatio, row.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, row.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, row.getTopRoundness(), 0.0f);
}
@Test
@@ -185,8 +185,8 @@
createSection(mFirst, mFirst),
createSection(null, mSecond)
});
- Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f);
}
@Test
@@ -195,8 +195,8 @@
createSection(mFirst, mFirst),
createSection(mSecond, null)
});
- Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(1.0f, mSecond.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mSecond.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mSecond.getTopRoundness(), 0.0f);
}
@Test
@@ -205,8 +205,8 @@
createSection(mFirst, null),
createSection(null, null)
});
- Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
}
@Test
@@ -215,8 +215,8 @@
createSection(mSecond, mSecond),
createSection(null, null)
});
- Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
}
@Test
@@ -226,8 +226,8 @@
createSection(mSecond, mSecond),
createSection(null, null)
});
- Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
}
@Test
@@ -238,8 +238,8 @@
createSection(mSecond, mSecond),
createSection(null, null)
});
- Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
}
@Test
@@ -250,8 +250,8 @@
createSection(mSecond, mSecond),
createSection(null, null)
});
- Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
}
@Test
@@ -262,8 +262,8 @@
createSection(mSecond, mSecond),
createSection(null, null)
});
- Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
}
@Test
@@ -274,8 +274,8 @@
createSection(mSecond, mSecond),
createSection(null, null)
});
- Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
}
@Test
@@ -286,8 +286,8 @@
createSection(mSecond, mSecond),
createSection(null, null)
});
- Assert.assertEquals(0.5f, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(0.5f, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(0.5f, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(0.5f, mFirst.getTopRoundness(), 0.0f);
}
@Test
@@ -298,8 +298,8 @@
createSection(null, null)
});
mFirst.setHeadsUpAnimatingAway(true);
- Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
}
@@ -312,8 +312,8 @@
});
mFirst.setHeadsUpAnimatingAway(true);
mFirst.setHeadsUpAnimatingAway(false);
- Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
- Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+ Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index a3c95dc..90061b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -129,6 +129,7 @@
@Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
@Mock private ShadeTransitionController mShadeTransitionController;
@Mock private FeatureFlags mFeatureFlags;
+ @Mock private NotificationTargetsHelper mNotificationTargetsHelper;
@Captor
private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
@@ -177,7 +178,8 @@
mStackLogger,
mLogger,
mNotificationStackSizeCalculator,
- mFeatureFlags
+ mFeatureFlags,
+ mNotificationTargetsHelper
);
when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 35c8b61..91aecd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -163,7 +163,7 @@
mStackScroller.setCentralSurfaces(mCentralSurfaces);
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
- when(mStackScrollLayoutController.getNoticationRoundessManager())
+ when(mStackScrollLayoutController.getNotificationRoundnessManager())
.thenReturn(mNotificationRoundnessManager);
mStackScroller.setController(mStackScrollLayoutController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
new file mode 100644
index 0000000..a2e9230
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
@@ -0,0 +1,107 @@
+package com.android.systemui.statusbar.notification.stack
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.util.mockito.mock
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for {@link NotificationTargetsHelper}. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationTargetsHelperTest : SysuiTestCase() {
+ lateinit var notificationTestHelper: NotificationTestHelper
+ private val sectionsManager: NotificationSectionsManager = mock()
+ private val stackScrollLayout: NotificationStackScrollLayout = mock()
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+ notificationTestHelper =
+ NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ }
+
+ private fun notificationTargetsHelper(
+ notificationGroupCorner: Boolean = true,
+ ) =
+ NotificationTargetsHelper(
+ FakeFeatureFlags().apply {
+ set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner)
+ }
+ )
+
+ @Test
+ fun targetsForFirstNotificationInGroup() {
+ val children = notificationTestHelper.createGroup(3).childrenContainer
+ val swiped = children.attachedChildren[0]
+
+ val actual =
+ notificationTargetsHelper()
+ .findRoundableTargets(
+ viewSwiped = swiped,
+ stackScrollLayout = stackScrollLayout,
+ sectionsManager = sectionsManager,
+ )
+
+ val expected =
+ RoundableTargets(
+ before = children.notificationHeaderWrapper, // group header
+ swiped = swiped,
+ after = children.attachedChildren[1],
+ )
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun targetsForMiddleNotificationInGroup() {
+ val children = notificationTestHelper.createGroup(3).childrenContainer
+ val swiped = children.attachedChildren[1]
+
+ val actual =
+ notificationTargetsHelper()
+ .findRoundableTargets(
+ viewSwiped = swiped,
+ stackScrollLayout = stackScrollLayout,
+ sectionsManager = sectionsManager,
+ )
+
+ val expected =
+ RoundableTargets(
+ before = children.attachedChildren[0],
+ swiped = swiped,
+ after = children.attachedChildren[2],
+ )
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun targetsForLastNotificationInGroup() {
+ val children = notificationTestHelper.createGroup(3).childrenContainer
+ val swiped = children.attachedChildren[2]
+
+ val actual =
+ notificationTargetsHelper()
+ .findRoundableTargets(
+ viewSwiped = swiped,
+ stackScrollLayout = stackScrollLayout,
+ sectionsManager = sectionsManager,
+ )
+
+ val expected =
+ RoundableTargets(
+ before = children.attachedChildren[1],
+ swiped = swiped,
+ after = null,
+ )
+ assertEquals(expected, actual)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 6ace404..915e999 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -23,8 +23,12 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.PendingIntent;
@@ -43,10 +47,14 @@
import android.testing.TestableLooper;
import android.view.ContentInfo;
import android.view.View;
+import android.view.ViewRootImpl;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
import android.widget.ImageButton;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
+import android.window.WindowOnBackInvokedDispatcher;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
@@ -67,6 +75,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -229,6 +238,67 @@
}
@Test
+ public void testPredictiveBack_registerAndUnregister() throws Exception {
+ NotificationTestHelper helper = new NotificationTestHelper(
+ mContext,
+ mDependency,
+ TestableLooper.get(this));
+ ExpandableNotificationRow row = helper.createRow();
+ RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+
+ ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+ WindowOnBackInvokedDispatcher backInvokedDispatcher = mock(
+ WindowOnBackInvokedDispatcher.class);
+ ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass(
+ OnBackInvokedCallback.class);
+ when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
+ view.setViewRootImpl(viewRoot);
+
+ /* verify that predictive back callback registered when RemoteInputView becomes visible */
+ view.onVisibilityAggregated(true);
+ verify(backInvokedDispatcher).registerOnBackInvokedCallback(
+ eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+ onBackInvokedCallbackCaptor.capture());
+
+ /* verify that same callback unregistered when RemoteInputView becomes invisible */
+ view.onVisibilityAggregated(false);
+ verify(backInvokedDispatcher).unregisterOnBackInvokedCallback(
+ eq(onBackInvokedCallbackCaptor.getValue()));
+ }
+
+ @Test
+ public void testUiPredictiveBack_openAndDispatchCallback() throws Exception {
+ NotificationTestHelper helper = new NotificationTestHelper(
+ mContext,
+ mDependency,
+ TestableLooper.get(this));
+ ExpandableNotificationRow row = helper.createRow();
+ RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+ ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+ WindowOnBackInvokedDispatcher backInvokedDispatcher = mock(
+ WindowOnBackInvokedDispatcher.class);
+ ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass(
+ OnBackInvokedCallback.class);
+ when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
+ view.setViewRootImpl(viewRoot);
+ view.onVisibilityAggregated(true);
+ view.setEditTextReferenceToSelf();
+
+ /* capture the callback during registration */
+ verify(backInvokedDispatcher).registerOnBackInvokedCallback(
+ eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+ onBackInvokedCallbackCaptor.capture());
+
+ view.focus();
+
+ /* invoke the captured callback */
+ onBackInvokedCallbackCaptor.getValue().onBackInvoked();
+
+ /* verify that the RemoteInputView goes away */
+ assertEquals(view.getVisibility(), View.GONE);
+ }
+
+ @Test
public void testUiEventLogging_openAndSend() throws Exception {
NotificationTestHelper helper = new NotificationTestHelper(
mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index cebe946..7af66f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -34,6 +34,8 @@
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
@@ -49,6 +51,7 @@
import org.mockito.MockitoAnnotations;
import java.util.Optional;
+import java.util.concurrent.Executor;
/**
* Tests for {@link WMShell}.
@@ -76,12 +79,14 @@
@Mock UserTracker mUserTracker;
@Mock ShellExecutor mSysUiMainExecutor;
@Mock FloatingTasks mFloatingTasks;
+ @Mock DesktopMode mDesktopMode;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip),
Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks),
+ Optional.of(mDesktopMode),
mCommandQueue, mConfigurationController, mKeyguardStateController,
mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer,
mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor);
@@ -103,4 +108,12 @@
verify(mOneHanded).registerTransitionCallback(any(OneHandedTransitionCallback.class));
verify(mOneHanded).registerEventCallback(any(OneHandedEventCallback.class));
}
+
+ @Test
+ public void initDesktopMode_registersListener() {
+ mWMShell.initDesktopMode(mDesktopMode);
+ verify(mDesktopMode).addListener(
+ any(DesktopModeTaskRepository.VisibleTasksListener.class),
+ any(Executor.class));
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
index 8d171be..69575a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
@@ -26,7 +26,9 @@
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatcher
import org.mockito.Mockito
+import org.mockito.Mockito.`when`
import org.mockito.stubbing.OngoingStubbing
+import org.mockito.stubbing.Stubber
/**
* Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
@@ -89,7 +91,8 @@
*
* @see Mockito.when
*/
-fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
+fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
+fun <T> Stubber.whenever(mock: T): T = `when`(mock)
/**
* A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 014d580..18c29fa 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -644,8 +644,8 @@
Permission bp = mRegistry.getPermission(info.name);
added = bp == null;
int fixedLevel = PermissionInfo.fixProtectionLevel(info.protectionLevel);
+ enforcePermissionCapLocked(info, tree);
if (added) {
- enforcePermissionCapLocked(info, tree);
bp = new Permission(info.name, tree.getPackageName(), Permission.TYPE_DYNAMIC);
} else if (!bp.isDynamic()) {
throw new SecurityException("Not allowed to modify non-dynamic permission "
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index bf4b65d..3a8fbbb 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -173,6 +173,7 @@
mWindowContainer = windowContainer;
// TODO: remove the frame provider for non-WindowState container.
mFrameProvider = frameProvider;
+ mOverrideFrames.clear();
mOverrideFrameProviders = overrideFrameProviders;
if (windowContainer == null) {
setServerVisible(false);
@@ -234,6 +235,8 @@
updateSourceFrameForServerVisibility();
if (mOverrideFrameProviders != null) {
+ // Not necessary to clear the mOverrideFrames here. It will be cleared every time the
+ // override frame provider updates.
for (int i = mOverrideFrameProviders.size() - 1; i >= 0; i--) {
final int windowType = mOverrideFrameProviders.keyAt(i);
final Rect overrideFrame;
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 867833a..509b1e6 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -184,19 +184,30 @@
}
void dispose() {
- while (!mOrganizedTaskFragments.isEmpty()) {
- final TaskFragment taskFragment = mOrganizedTaskFragments.get(0);
- // Cleanup before remove to prevent it from sending any additional event, such as
- // #onTaskFragmentVanished, to the removed organizer.
+ for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) {
+ // Cleanup the TaskFragmentOrganizer from all TaskFragments it organized before
+ // removing the windows to prevent it from adding any additional TaskFragment
+ // pending event.
+ final TaskFragment taskFragment = mOrganizedTaskFragments.get(i);
taskFragment.onTaskFragmentOrganizerRemoved();
- taskFragment.removeImmediately();
- mOrganizedTaskFragments.remove(taskFragment);
}
+
+ // Defer to avoid unnecessary layout when there are multiple TaskFragments removal.
+ mAtmService.deferWindowLayout();
+ try {
+ while (!mOrganizedTaskFragments.isEmpty()) {
+ final TaskFragment taskFragment = mOrganizedTaskFragments.remove(0);
+ taskFragment.removeImmediately();
+ }
+ } finally {
+ mAtmService.continueWindowLayout();
+ }
+
for (int i = mDeferredTransitions.size() - 1; i >= 0; i--) {
// Cleanup any running transaction to unblock the current transition.
onTransactionFinished(mDeferredTransitions.keyAt(i));
}
- mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/);
+ mOrganizer.asBinder().unlinkToDeath(this, 0 /* flags */);
}
@NonNull
@@ -426,7 +437,6 @@
@Override
public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
- validateAndGetState(organizer);
final int pid = Binder.getCallingPid();
final long uid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
@@ -607,6 +617,13 @@
int opType, @NonNull Throwable exception) {
validateAndGetState(organizer);
Slog.w(TAG, "onTaskFragmentError ", exception);
+ final PendingTaskFragmentEvent vanishedEvent = taskFragment != null
+ ? getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_VANISHED)
+ : null;
+ if (vanishedEvent != null) {
+ // No need to notify if the TaskFragment has been removed.
+ return;
+ }
addPendingEvent(new PendingTaskFragmentEvent.Builder(
PendingTaskFragmentEvent.EVENT_ERROR, organizer)
.setErrorCallbackToken(errorCallbackToken)
@@ -690,11 +707,17 @@
}
private void removeOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
- final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+ final TaskFragmentOrganizerState state = mTaskFragmentOrganizerState.get(
+ organizer.asBinder());
+ if (state == null) {
+ Slog.w(TAG, "The organizer has already been removed.");
+ return;
+ }
+ // Remove any pending event of this organizer first because state.dispose() may trigger
+ // event dispatch as result of surface placement.
+ mPendingTaskFragmentEvents.remove(organizer.asBinder());
// remove all of the children of the organized TaskFragment
state.dispose();
- // Remove any pending event of this organizer.
- mPendingTaskFragmentEvents.remove(organizer.asBinder());
mTaskFragmentOrganizerState.remove(organizer.asBinder());
}
@@ -878,23 +901,6 @@
return null;
}
- private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) {
- if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR
- // Always send parent info changed to update task visibility
- || event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) {
- return true;
- }
-
- 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
- // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment.
- return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED
- && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty();
- }
-
void dispatchPendingEvents() {
if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred()
|| mPendingTaskFragmentEvents.isEmpty()) {
@@ -908,37 +914,19 @@
}
}
- void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
+ private void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
@NonNull List<PendingTaskFragmentEvent> pendingEvents) {
if (pendingEvents.isEmpty()) {
return;
}
-
- final ArrayList<Task> visibleTasks = new ArrayList<>();
- final ArrayList<Task> invisibleTasks = new ArrayList<>();
- final ArrayList<PendingTaskFragmentEvent> candidateEvents = new ArrayList<>();
- for (int i = 0, n = pendingEvents.size(); i < n; i++) {
- final PendingTaskFragmentEvent event = pendingEvents.get(i);
- final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null;
- // TODO(b/251132298): move visibility check to the client side.
- if (task != null && (task.lastActiveTime <= event.mDeferTime
- || !(isTaskVisible(task, visibleTasks, invisibleTasks)
- || shouldSendEventWhenTaskInvisible(event)))) {
- // Defer sending events to the TaskFragment until the host task is active again.
- event.mDeferTime = task.lastActiveTime;
- continue;
- }
- candidateEvents.add(event);
- }
- final int numEvents = candidateEvents.size();
- if (numEvents == 0) {
+ if (shouldDeferPendingEvents(state, pendingEvents)) {
return;
}
-
mTmpTaskSet.clear();
+ final int numEvents = pendingEvents.size();
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
for (int i = 0; i < numEvents; i++) {
- final PendingTaskFragmentEvent event = candidateEvents.get(i);
+ final PendingTaskFragmentEvent event = pendingEvents.get(i);
if (event.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED
|| event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
final Task task = event.mTaskFragment.getTask();
@@ -954,7 +942,47 @@
}
mTmpTaskSet.clear();
state.dispatchTransaction(transaction);
- pendingEvents.removeAll(candidateEvents);
+ pendingEvents.clear();
+ }
+
+ /**
+ * Whether or not to defer sending the events to the organizer to avoid waking the app process
+ * when it is in background. We want to either send all events or none to avoid inconsistency.
+ */
+ private boolean shouldDeferPendingEvents(@NonNull TaskFragmentOrganizerState state,
+ @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
+ final ArrayList<Task> visibleTasks = new ArrayList<>();
+ final ArrayList<Task> invisibleTasks = new ArrayList<>();
+ for (int i = 0, n = pendingEvents.size(); i < n; i++) {
+ final PendingTaskFragmentEvent event = pendingEvents.get(i);
+ if (event.mEventType != PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED
+ && event.mEventType != PendingTaskFragmentEvent.EVENT_INFO_CHANGED
+ && event.mEventType != PendingTaskFragmentEvent.EVENT_APPEARED) {
+ // Send events for any other types.
+ return false;
+ }
+
+ // Check if we should send the event given the Task visibility and events.
+ final Task task;
+ if (event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) {
+ task = event.mTask;
+ } else {
+ task = event.mTaskFragment.getTask();
+ }
+ if (task.lastActiveTime > event.mDeferTime
+ && isTaskVisible(task, visibleTasks, invisibleTasks)) {
+ // Send events when the app has at least one visible Task.
+ return false;
+ } else if (shouldSendEventWhenTaskInvisible(task, state, event)) {
+ // Sent events even if the Task is invisible.
+ return false;
+ }
+
+ // Defer sending events to the organizer until the host task is active (visible) again.
+ event.mDeferTime = task.lastActiveTime;
+ }
+ // Defer for invisible Task.
+ return true;
}
private static boolean isTaskVisible(@NonNull Task task,
@@ -975,6 +1003,28 @@
}
}
+ private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task,
+ @NonNull TaskFragmentOrganizerState state,
+ @NonNull PendingTaskFragmentEvent event) {
+ final TaskFragmentParentInfo lastParentInfo = state.mLastSentTaskFragmentParentInfos
+ .get(task.mTaskId);
+ if (lastParentInfo == null || lastParentInfo.isVisible()) {
+ // When the Task was visible, or when there was no Task info changed sent (in which case
+ // the organizer will consider it as visible by default), always send the event to
+ // update the Task visibility.
+ return true;
+ }
+ if (event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
+ // Send info changed if the TaskFragment is becoming empty/non-empty so the
+ // organizer can choose whether or not to remove the TaskFragment.
+ final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos
+ .get(event.mTaskFragment);
+ final boolean isEmpty = event.mTaskFragment.getNonFinishingActivityCount() == 0;
+ return lastInfo == null || lastInfo.isEmpty() != isEmpty;
+ }
+ return false;
+ }
+
void dispatchPendingInfoChangedEvent(@NonNull TaskFragment taskFragment) {
final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment,
PendingTaskFragmentEvent.EVENT_INFO_CHANGED);
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 0b23359..4202f46 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -56,6 +56,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -91,6 +92,7 @@
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
import java.util.List;
@@ -762,6 +764,50 @@
}
@Test
+ public void testOrganizerRemovedWithPendingEvents() {
+ final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
+ .setCreateParentTask()
+ .setOrganizer(mOrganizer)
+ .setFragmentToken(mFragmentToken)
+ .build();
+ final TaskFragment tf1 = new TaskFragmentBuilder(mAtm)
+ .setCreateParentTask()
+ .setOrganizer(mOrganizer)
+ .setFragmentToken(new Binder())
+ .build();
+ assertTrue(tf0.isOrganizedTaskFragment());
+ assertTrue(tf1.isOrganizedTaskFragment());
+ assertTrue(tf0.isAttached());
+ assertTrue(tf0.isAttached());
+
+ // Mock the behavior that remove TaskFragment can trigger event dispatch.
+ final Answer<Void> removeImmediately = invocation -> {
+ invocation.callRealMethod();
+ mController.dispatchPendingEvents();
+ return null;
+ };
+ doAnswer(removeImmediately).when(tf0).removeImmediately();
+ doAnswer(removeImmediately).when(tf1).removeImmediately();
+
+ // Add pending events.
+ mController.onTaskFragmentAppeared(mIOrganizer, tf0);
+ mController.onTaskFragmentAppeared(mIOrganizer, tf1);
+
+ // Remove organizer.
+ mController.unregisterOrganizer(mIOrganizer);
+ mController.dispatchPendingEvents();
+
+ // Nothing should happen after the organizer is removed.
+ verify(mOrganizer, never()).onTransactionReady(any());
+
+ // TaskFragments should be removed.
+ assertFalse(tf0.isOrganizedTaskFragment());
+ assertFalse(tf1.isOrganizedTaskFragment());
+ assertFalse(tf0.isAttached());
+ assertFalse(tf0.isAttached());
+ }
+
+ @Test
public void testTaskFragmentInPip_startActivityInTaskFragment() {
setupTaskFragmentInPip();
final ActivityRecord activity = mTaskFragment.getTopMostActivity();
@@ -874,29 +920,87 @@
@Test
public void testDeferPendingTaskFragmentEventsOfInvisibleTask() {
- // Task - TaskFragment - Activity.
final Task task = createTask(mDisplayContent);
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.setOrganizer(mOrganizer)
.setFragmentToken(mFragmentToken)
.build();
-
- // Mock the task to invisible
doReturn(false).when(task).shouldBeVisible(any());
- // Sending events
- taskFragment.mTaskFragmentAppearedSent = true;
+ // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+ mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
+ mController.dispatchPendingEvents();
+ verify(mOrganizer).onTransactionReady(any());
+
+ // Verify that events were not sent when the Task is in background.
+ clearInvocations(mOrganizer);
+ final Rect bounds = new Rect(0, 0, 500, 1000);
+ task.setBoundsUnchecked(bounds);
+ mController.onTaskFragmentParentInfoChanged(mIOrganizer, task);
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
-
- // Verifies that event was not sent
verify(mOrganizer, never()).onTransactionReady(any());
+
+ // Verify that the events were sent when the Task becomes visible.
+ doReturn(true).when(task).shouldBeVisible(any());
+ task.lastActiveTime++;
+ mController.dispatchPendingEvents();
+ verify(mOrganizer).onTransactionReady(any());
+ }
+
+ @Test
+ public void testSendAllPendingTaskFragmentEventsWhenAnyTaskIsVisible() {
+ // Invisible Task.
+ final Task invisibleTask = createTask(mDisplayContent);
+ final TaskFragment invisibleTaskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(invisibleTask)
+ .setOrganizer(mOrganizer)
+ .setFragmentToken(mFragmentToken)
+ .build();
+ doReturn(false).when(invisibleTask).shouldBeVisible(any());
+
+ // Visible Task.
+ final IBinder fragmentToken = new Binder();
+ final Task visibleTask = createTask(mDisplayContent);
+ final TaskFragment visibleTaskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(visibleTask)
+ .setOrganizer(mOrganizer)
+ .setFragmentToken(fragmentToken)
+ .build();
+ doReturn(true).when(invisibleTask).shouldBeVisible(any());
+
+ // Sending events
+ invisibleTaskFragment.mTaskFragmentAppearedSent = true;
+ visibleTaskFragment.mTaskFragmentAppearedSent = true;
+ mController.onTaskFragmentInfoChanged(mIOrganizer, invisibleTaskFragment);
+ mController.onTaskFragmentInfoChanged(mIOrganizer, visibleTaskFragment);
+ mController.dispatchPendingEvents();
+
+ // Verify that both events are sent.
+ verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
+ final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
+ final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
+
+ // There should be two Task info changed with two TaskFragment info changed.
+ assertEquals(4, changes.size());
+ // Invisible Task info changed
+ assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(0).getType());
+ assertEquals(invisibleTask.mTaskId, changes.get(0).getTaskId());
+ // Invisible TaskFragment info changed
+ assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(1).getType());
+ assertEquals(invisibleTaskFragment.getFragmentToken(),
+ changes.get(1).getTaskFragmentToken());
+ // Visible Task info changed
+ assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(2).getType());
+ assertEquals(visibleTask.mTaskId, changes.get(2).getTaskId());
+ // Visible TaskFragment info changed
+ assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(3).getType());
+ assertEquals(visibleTaskFragment.getFragmentToken(), changes.get(3).getTaskFragmentToken());
}
@Test
public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() {
- // Task - TaskFragment - Activity.
final Task task = createTask(mDisplayContent);
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
@@ -905,24 +1009,26 @@
.createActivityCount(1)
.build();
final ActivityRecord activity = taskFragment.getTopMostActivity();
-
- // Mock the task to invisible
doReturn(false).when(task).shouldBeVisible(any());
taskFragment.setResumedActivity(null, "test");
- // Sending events
- taskFragment.mTaskFragmentAppearedSent = true;
+ // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+ mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
+ mController.dispatchPendingEvents();
+ verify(mOrganizer).onTransactionReady(any());
+
+ // Verify the info changed event is not sent because the Task is invisible
+ clearInvocations(mOrganizer);
+ final Rect bounds = new Rect(0, 0, 500, 1000);
+ task.setBoundsUnchecked(bounds);
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
-
- // Verifies that event was not sent
verify(mOrganizer, never()).onTransactionReady(any());
- // Mock the task becomes visible, and activity resumed
+ // Mock the task becomes visible, and activity resumed. Verify the info changed event is
+ // sent.
doReturn(true).when(task).shouldBeVisible(any());
taskFragment.setResumedActivity(activity, "test");
-
- // Verifies that event is sent.
mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any());
}
@@ -977,25 +1083,24 @@
final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
// Add another activity in the Task so that it always contains a non-finishing activity.
createActivityRecord(task);
- assertTrue(task.shouldBeVisible(null));
+ doReturn(false).when(task).shouldBeVisible(any());
- // Dispatch pending info changed event from creating the activity
- taskFragment.mTaskFragmentAppearedSent = true;
- mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+ // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+ mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any());
- // Verify the info changed callback is not called when the task is invisible
+ // Verify the info changed event is not sent because the Task is invisible
clearInvocations(mOrganizer);
- doReturn(false).when(task).shouldBeVisible(any());
+ final Rect bounds = new Rect(0, 0, 500, 1000);
+ task.setBoundsUnchecked(bounds);
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTransactionReady(any());
- // Finish the embedded activity, and verify the info changed callback is called because the
+ // Finish the embedded activity, and verify the info changed event is sent because the
// TaskFragment is becoming empty.
embeddedActivity.finishing = true;
- mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTransactionReady(any());
}