Merge "Temporarily ignoring flaky test checking mDevice.pressRecentApps()" into ub-launcher3-master
diff --git a/go/quickstep/src/com/android/quickstep/TaskActionController.java b/go/quickstep/src/com/android/quickstep/TaskActionController.java
index b2d495b..77b287b 100644
--- a/go/quickstep/src/com/android/quickstep/TaskActionController.java
+++ b/go/quickstep/src/com/android/quickstep/TaskActionController.java
@@ -71,7 +71,6 @@
* Clears all tasks and updates the model and view.
*/
public void clearAllTasks() {
- // TODO: Play an animation so transition is more natural.
int count = mAdapter.getItemCount();
ActivityManagerWrapper.getInstance().removeAllRecentTasks();
mLoader.clearAllTasks();
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
index e56cc51..c98eca6 100644
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
@@ -75,8 +75,20 @@
// Task list has updated.
return;
}
- holder.bindTask(tasks.get(position));
-
+ Task task = tasks.get(position);
+ holder.bindTask(task);
+ mLoader.loadTaskIconAndLabel(task, () -> {
+ // Ensure holder still has the same task.
+ if (task.equals(holder.getTask())) {
+ holder.getTaskItemView().setIcon(task.icon);
+ holder.getTaskItemView().setLabel(task.titleDescription);
+ }
+ });
+ mLoader.loadTaskThumbnail(task, () -> {
+ if (task.equals(holder.getTask())) {
+ holder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
+ }
+ });
}
@Override
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
index a89229f..744afd7 100644
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ b/go/quickstep/src/com/android/quickstep/TaskHolder.java
@@ -35,17 +35,18 @@
mTaskItemView = itemView;
}
+ public TaskItemView getTaskItemView() {
+ return mTaskItemView;
+ }
+
/**
- * Bind task content to the view. This includes the task icon and title as well as binding
- * input handlers such as which task to launch/remove.
+ * Bind a task to the holder, resetting the view and preparing it for content to load in.
*
* @param task the task to bind to the view
*/
public void bindTask(Task task) {
mTask = task;
- mTaskItemView.setLabel(task.titleDescription);
- mTaskItemView.setIcon(task.icon);
- mTaskItemView.setThumbnail(task.thumbnail.thumbnail);
+ mTaskItemView.resetTaskItemView();
}
/**
diff --git a/go/quickstep/src/com/android/quickstep/TaskListLoader.java b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
index c86c24e..1234989 100644
--- a/go/quickstep/src/com/android/quickstep/TaskListLoader.java
+++ b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
@@ -25,7 +25,6 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/**
@@ -39,35 +38,48 @@
private ArrayList<Task> mTaskList = new ArrayList<>();
private int mTaskListChangeId;
+ private RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
+ Task foundTask = null;
+ for (Task task : mTaskList) {
+ if (task.key.id == taskId) {
+ foundTask = task;
+ break;
+ }
+ }
+ if (foundTask != null) {
+ foundTask.thumbnail = thumbnailData;
+ }
+ return foundTask;
+ };
public TaskListLoader(Context context) {
mRecentsModel = RecentsModel.INSTANCE.get(context);
+ mRecentsModel.addThumbnailChangeListener(listener);
}
/**
- * Returns the current task list as of the last completed load (see
- * {@link #loadTaskList}) as a read-only list. This list of tasks is guaranteed to always have
- * all its task content loaded.
+ * Returns the current task list as of the last completed load (see {@link #loadTaskList}) as a
+ * read-only list. This list of tasks is not guaranteed to have all content loaded.
*
- * @return the current list of tasks w/ all content loaded
+ * @return the current list of tasks
*/
public List<Task> getCurrentTaskList() {
return Collections.unmodifiableList(mTaskList);
}
/**
- * Fetches the most recent tasks and updates the task list asynchronously. In addition it
- * loads the content for each task (icon and label). The callback and task list being updated
- * only occur when all task content is fully loaded and up-to-date.
+ * Fetches the most recent tasks and updates the task list asynchronously. This call does not
+ * provide guarantees the task content (icon, thumbnail, label) are loaded but will fill in
+ * what it has. May run the callback immediately if there have been no changes in the task
+ * list.
*
- * @param onTasksLoadedCallback callback for when the tasks are fully loaded. Done on the UI
- * thread
+ * @param onLoadedCallback callback to run when task list is loaded
*/
- public void loadTaskList(@Nullable Consumer<ArrayList<Task>> onTasksLoadedCallback) {
+ public void loadTaskList(@Nullable Consumer<ArrayList<Task>> onLoadedCallback) {
if (mRecentsModel.isTaskListValid(mTaskListChangeId)) {
// Current task list is already up to date. No need to update.
- if (onTasksLoadedCallback != null) {
- onTasksLoadedCallback.accept(mTaskList);
+ if (onLoadedCallback != null) {
+ onLoadedCallback.accept(mTaskList);
}
return;
}
@@ -76,16 +88,46 @@
// Reverse tasks to put most recent at the bottom of the view
Collections.reverse(tasks);
// Load task content
- loadTaskContents(tasks, () -> {
- mTaskList = tasks;
- if (onTasksLoadedCallback != null) {
- onTasksLoadedCallback.accept(mTaskList);
+ for (Task task : tasks) {
+ int loadedPos = mTaskList.indexOf(task);
+ if (loadedPos == -1) {
+ continue;
}
- });
+ Task loadedTask = mTaskList.get(loadedPos);
+ task.icon = loadedTask.icon;
+ task.titleDescription = loadedTask.titleDescription;
+ task.thumbnail = loadedTask.thumbnail;
+ }
+ mTaskList = tasks;
+ onLoadedCallback.accept(tasks);
});
}
/**
+ * Load task icon and label asynchronously if it is not already loaded in the task. If the task
+ * already has an icon, this calls the callback immediately.
+ *
+ * @param task task to update with icon + label
+ * @param onLoadedCallback callback to run when task has icon and label
+ */
+ public void loadTaskIconAndLabel(Task task, @Nullable Runnable onLoadedCallback) {
+ mRecentsModel.getIconCache().updateIconInBackground(task,
+ loadedTask -> onLoadedCallback.run());
+ }
+
+ /**
+ * Load thumbnail asynchronously if not already loaded in the task. If the task already has a
+ * thumbnail or if the thumbnail is cached, this calls the callback immediately.
+ *
+ * @param task task to update with the thumbnail
+ * @param onLoadedCallback callback to run when task has thumbnail
+ */
+ public void loadTaskThumbnail(Task task, @Nullable Runnable onLoadedCallback) {
+ mRecentsModel.getThumbnailCache().updateThumbnailInBackground(task,
+ thumbnail -> onLoadedCallback.run());
+ }
+
+ /**
* Removes the task from the current task list.
*/
void removeTask(Task task) {
@@ -98,42 +140,4 @@
void clearAllTasks() {
mTaskList.clear();
}
-
- /**
- * Loads task content for a list of tasks, including the label, icon, and thumbnail. For content
- * that isn't cached, load the content asynchronously in the background.
- *
- * @param tasksToLoad list of tasks that need to load their content
- * @param onFullyLoadedCallback runnable to run after all tasks have loaded their content
- */
- private void loadTaskContents(ArrayList<Task> tasksToLoad,
- @Nullable Runnable onFullyLoadedCallback) {
- // Make two load requests per task, one for the icon/title and one for the thumbnail.
- AtomicInteger loadRequestsCount = new AtomicInteger(tasksToLoad.size() * 2);
- Runnable itemLoadedRunnable = () -> {
- if (loadRequestsCount.decrementAndGet() == 0 && onFullyLoadedCallback != null) {
- onFullyLoadedCallback.run();
- }
- };
- for (Task task : tasksToLoad) {
- // Load icon and title.
- int index = mTaskList.indexOf(task);
- if (index >= 0) {
- // If we've already loaded the task and have its content then just copy it over.
- Task loadedTask = mTaskList.get(index);
- task.titleDescription = loadedTask.titleDescription;
- task.icon = loadedTask.icon;
- itemLoadedRunnable.run();
- } else {
- // Otherwise, load the content in the background.
- mRecentsModel.getIconCache().updateIconInBackground(task,
- loadedTask -> itemLoadedRunnable.run());
- }
-
- // Load the thumbnail. May return immediately and synchronously if the thumbnail is
- // cached.
- mRecentsModel.getThumbnailCache().updateThumbnailInBackground(task,
- thumbnail -> itemLoadedRunnable.run());
- }
- }
}
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index a1d62c2..1e01725 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -19,6 +19,10 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.util.FloatProperty;
@@ -65,6 +69,10 @@
}
};
private static final long CROSSFADE_DURATION = 300;
+ private static final long ITEM_ANIMATE_OUT_DURATION = 150;
+ private static final long ITEM_ANIMATE_OUT_DELAY_BETWEEN = 40;
+ private static final float ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO = .25f;
+ private static final long CLEAR_ALL_FADE_DELAY = 120;
/**
* A ratio representing the view's relative placement within its padded space. For example, 0
@@ -119,7 +127,7 @@
});
View clearAllView = findViewById(R.id.clear_all_button);
- clearAllView.setOnClickListener(v -> mTaskActionController.clearAllTasks());
+ clearAllView.setOnClickListener(v -> animateClearAllTasks());
}
}
@@ -195,6 +203,76 @@
}
/**
+ * Clear all tasks and animate out.
+ */
+ private void animateClearAllTasks() {
+ TaskItemView[] itemViews = getTaskViews();
+
+ AnimatorSet clearAnim = new AnimatorSet();
+ long currentDelay = 0;
+
+ // Animate each item view to the right and fade out.
+ for (TaskItemView itemView : itemViews) {
+ PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
+ 0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
+ PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
+ ObjectAnimator itemAnim = ObjectAnimator.ofPropertyValuesHolder(itemView,
+ transXproperty, alphaProperty);
+ itemAnim.setDuration(ITEM_ANIMATE_OUT_DURATION);
+ itemAnim.setStartDelay(currentDelay);
+
+ clearAnim.play(itemAnim);
+ currentDelay += ITEM_ANIMATE_OUT_DELAY_BETWEEN;
+ }
+
+ // Animate view fading and leave recents when faded enough.
+ ValueAnimator contentAlpha = ValueAnimator.ofFloat(1.0f, 0f)
+ .setDuration(CROSSFADE_DURATION);
+ contentAlpha.setStartDelay(CLEAR_ALL_FADE_DELAY);
+ contentAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ private boolean mLeftRecents = false;
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ mContentView.setAlpha((float) valueAnimator.getAnimatedValue());
+ // Leave recents while fading out.
+ if ((float) valueAnimator.getAnimatedValue() < .5f && !mLeftRecents) {
+ mActivityHelper.leaveRecents();
+ mLeftRecents = true;
+ }
+ }
+ });
+
+ clearAnim.play(contentAlpha);
+ clearAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ for (TaskItemView itemView : itemViews) {
+ itemView.setTranslationX(0);
+ itemView.setAlpha(1.0f);
+ }
+ mContentView.setVisibility(GONE);
+ mTaskActionController.clearAllTasks();
+ }
+ });
+ clearAnim.start();
+ }
+
+ /**
+ * Get attached task item views ordered by most recent.
+ *
+ * @return array of attached task item views
+ */
+ private TaskItemView[] getTaskViews() {
+ int taskCount = mTaskRecyclerView.getChildCount();
+ TaskItemView[] itemViews = new TaskItemView[taskCount];
+ for (int i = 0; i < taskCount; i ++) {
+ itemViews[i] = (TaskItemView) mTaskRecyclerView.getChildAt(i);
+ }
+ return itemViews;
+ }
+
+ /**
* Update the content view so that the appropriate view is shown based off the current list
* of tasks.
*/
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
index 373f107..d831b20 100644
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
@@ -17,6 +17,7 @@
import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
@@ -24,6 +25,8 @@
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.R;
/**
@@ -31,12 +34,16 @@
*/
public final class TaskItemView extends LinearLayout {
+ private static final String DEFAULT_LABEL = "...";
+ private final Drawable mDefaultIcon;
private TextView mLabelView;
private ImageView mIconView;
private ImageView mThumbnailView;
public TaskItemView(Context context, AttributeSet attrs) {
super(context, attrs);
+ mDefaultIcon = context.getResources().getDrawable(
+ android.R.drawable.sym_def_app_icon, context.getTheme());
}
@Override
@@ -48,33 +55,56 @@
}
/**
- * Set the label for the task item.
+ * Resets task item view to default values.
+ */
+ public void resetTaskItemView() {
+ setLabel(DEFAULT_LABEL);
+ setIcon(null);
+ setThumbnail(null);
+ }
+
+ /**
+ * Set the label for the task item. Sets to a default label if null.
*
* @param label task label
*/
- public void setLabel(String label) {
+ public void setLabel(@Nullable String label) {
+ if (label == null) {
+ mLabelView.setText(DEFAULT_LABEL);
+ return;
+ }
mLabelView.setText(label);
}
/**
- * Set the icon for the task item.
+ * Set the icon for the task item. Sets to a default icon if null.
*
* @param icon task icon
*/
- public void setIcon(Drawable icon) {
+ public void setIcon(@Nullable Drawable icon) {
// TODO: Scale the icon up based off the padding on the side
// The icon proper is actually smaller than the drawable and has "padding" on the side for
// the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
// view if we want the icon to be flush with the bottom of the thumbnail.
+ if (icon == null) {
+ mIconView.setImageDrawable(mDefaultIcon);
+ return;
+ }
mIconView.setImageDrawable(icon);
}
/**
- * Set the task thumbnail for the task.
+ * Set the task thumbnail for the task. Sets to a default thumbnail if null.
*
* @param thumbnail task thumbnail for the task
*/
- public void setThumbnail(Bitmap thumbnail) {
+ public void setThumbnail(@Nullable Bitmap thumbnail) {
+ if (thumbnail == null) {
+ mThumbnailView.setImageBitmap(null);
+ mThumbnailView.setBackgroundColor(Color.GRAY);
+ return;
+ }
+ mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
mThumbnailView.setImageBitmap(thumbnail);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index b71f790..00257a5 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -347,12 +347,7 @@
log(action = "0-button: from another app");
assertTrue("Launcher is visible, don't know how to go home",
!mDevice.hasObject(By.pkg(getLauncherPackageName())));
- final UiObject2 navBar = waitForSystemUiObject("navigation_bar_frame");
-
- swipe(
- navBar.getVisibleBounds().centerX(), navBar.getVisibleBounds().centerY(),
- navBar.getVisibleBounds().centerX(), 0,
- BACKGROUND_APP_STATE_ORDINAL, ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME);
+ mDevice.pressHome();
}
} else {
log(action = "clicking home button");