diff --git a/Android.mk b/Android.mk
index a817415ea6..6568a263e8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -251,7 +251,7 @@ LOCAL_PROGUARD_ENABLED := full
LOCAL_PACKAGE_NAME := Launcher3QuickStepGo
LOCAL_PRIVILEGED_MODULE := true
LOCAL_PRODUCT_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
+LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep Launcher3GoIconRecents
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
LOCAL_FULL_LIBS_MANIFEST_FILES := \
@@ -297,7 +297,8 @@ LOCAL_PROGUARD_ENABLED := full
LOCAL_PACKAGE_NAME := Launcher3GoIconRecents
LOCAL_PRIVILEGED_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
+LOCAL_PRODUCT_MODULE := true
+LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep Launcher3QuickStepGo
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
LOCAL_FULL_LIBS_MANIFEST_FILES := \
diff --git a/res/layout/hint_container.xml b/go/quickstep/res/drawable/default_thumbnail.xml
similarity index 76%
rename from res/layout/hint_container.xml
rename to go/quickstep/res/drawable/default_thumbnail.xml
index 75aa913411..0a2dbf03de 100644
--- a/res/layout/hint_container.xml
+++ b/go/quickstep/res/drawable/default_thumbnail.xml
@@ -14,4 +14,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
\ No newline at end of file
+
+
+
+
diff --git a/quickstep/recents_ui_overrides/res/layout/hint_container.xml b/go/quickstep/res/drawable/empty_content_box.xml
similarity index 74%
rename from quickstep/recents_ui_overrides/res/layout/hint_container.xml
rename to go/quickstep/res/drawable/empty_content_box.xml
index f8723fc81b..a4883889fe 100644
--- a/quickstep/recents_ui_overrides/res/layout/hint_container.xml
+++ b/go/quickstep/res/drawable/empty_content_box.xml
@@ -14,10 +14,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
\ No newline at end of file
+ android:shape="rectangle">
+
+
+
+
\ No newline at end of file
diff --git a/go/quickstep/res/layout/clear_all_button.xml b/go/quickstep/res/layout/clear_all_button.xml
new file mode 100644
index 0000000000..be76d53661
--- /dev/null
+++ b/go/quickstep/res/layout/clear_all_button.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
diff --git a/go/quickstep/res/layout/icon_recents_root_view.xml b/go/quickstep/res/layout/icon_recents_root_view.xml
index fddb1d347e..6fb7e19d13 100644
--- a/go/quickstep/res/layout/icon_recents_root_view.xml
+++ b/go/quickstep/res/layout/icon_recents_root_view.xml
@@ -19,31 +19,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
-
-
-
-
+ android:scrollbars="none"/>
-
+ android:layout_marginTop="16dp">
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
-
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
-
-
- 8dp
- 60dp
- 60dp
- 36dp
- 36dp
-
- 106dp
- 36dp
-
\ No newline at end of file
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 6730e975ea..d20910fd24 100644
--- a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -18,6 +18,7 @@ package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
@@ -51,6 +52,9 @@ public class OverviewState extends LauncherState {
public void onStateEnabled(Launcher launcher) {
IconRecentsView recentsView = launcher.getOverviewPanel();
recentsView.onBeginTransitionToOverview();
+ // Request orientation be set to unspecified, letting the system decide the best
+ // orientation.
+ launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
}
@Override
diff --git a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index d1d697c0cd..c228bb94fe 100644
--- a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -131,10 +131,11 @@ final class AppToOverviewAnimationProvider imple
return anim;
}
- View thumbnailView = mRecentsView.getThumbnailViewForTask(mTargetTaskId);
+ View thumbnailView = mRecentsView.getBottomThumbnailView();
if (thumbnailView == null) {
- // TODO: We should either 1) guarantee the view is loaded before attempting this
- // or 2) have a backup animation.
+ // This can be null if there were previously 0 tasks and the recycler view has not had
+ // enough time to take in the data change, bind a new view, and lay out the new view.
+ // TODO: Have a fallback to animate to
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "No thumbnail view for running task. Using stub animation.");
}
diff --git a/go/quickstep/src/com/android/quickstep/ClearAllHolder.java b/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
new file mode 100644
index 0000000000..ce87171f4b
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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.quickstep;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * Holder for clear all button view in task recycler view.
+ */
+final class ClearAllHolder extends ViewHolder {
+ public ClearAllHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java b/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
new file mode 100644
index 0000000000..1b6f2e34d9
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2019 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.quickstep;
+
+import static android.view.View.ALPHA;
+
+import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
+import static com.android.quickstep.views.TaskItemView.CONTENT_TRANSITION_PROGRESS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import androidx.recyclerview.widget.SimpleItemAnimator;
+
+import com.android.quickstep.views.TaskItemView;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * An item animator that is only set and used for the transition from the empty loading UI to
+ * the filled task content UI. The animation starts from the bottom to top, changing all valid
+ * empty item views to be filled and removing all extra empty views.
+ */
+public final class ContentFillItemAnimator extends SimpleItemAnimator {
+
+ private static final class PendingAnimation {
+ ViewHolder viewHolder;
+ int animType;
+
+ PendingAnimation(ViewHolder vh, int type) {
+ viewHolder = vh;
+ animType = type;
+ }
+ }
+
+ private static final int ANIM_TYPE_REMOVE = 0;
+ private static final int ANIM_TYPE_CHANGE = 1;
+
+ private static final int ITEM_BETWEEN_DELAY = 40;
+ private static final int ITEM_CHANGE_DURATION = 150;
+ private static final int ITEM_REMOVE_DURATION = 150;
+
+ /**
+ * Animations that have been registered to occur together at the next call of
+ * {@link #runPendingAnimations()} but have not started.
+ */
+ private final ArrayList mPendingAnims = new ArrayList<>();
+
+ /**
+ * Animations that have started and are running.
+ */
+ private final ArrayList mRunningAnims = new ArrayList<>();
+
+ private Runnable mOnFinishRunnable;
+
+ /**
+ * Set runnable to run after the content fill animation is fully completed.
+ *
+ * @param runnable runnable to run on end
+ */
+ public void setOnAnimationFinishedRunnable(Runnable runnable) {
+ mOnFinishRunnable = runnable;
+ }
+
+ @Override
+ public void setChangeDuration(long changeDuration) {
+ throw new UnsupportedOperationException("Cascading item animator cannot have animation "
+ + "duration changed.");
+ }
+
+ @Override
+ public void setRemoveDuration(long removeDuration) {
+ throw new UnsupportedOperationException("Cascading item animator cannot have animation "
+ + "duration changed.");
+ }
+
+ @Override
+ public boolean animateRemove(ViewHolder holder) {
+ PendingAnimation pendAnim = new PendingAnimation(holder, ANIM_TYPE_REMOVE);
+ mPendingAnims.add(pendAnim);
+ return true;
+ }
+
+ private void animateRemoveImpl(ViewHolder holder, long startDelay) {
+ final View view = holder.itemView;
+ if (holder.itemView.getAlpha() == 0) {
+ // View is already visually removed. We can just get rid of it now.
+ view.setAlpha(1.0f);
+ dispatchRemoveFinished(holder);
+ dispatchFinishedWhenDone();
+ return;
+ }
+ final ObjectAnimator anim = ObjectAnimator.ofFloat(
+ holder.itemView, ALPHA, holder.itemView.getAlpha(), 0.0f);
+ anim.setDuration(ITEM_REMOVE_DURATION).setStartDelay(startDelay);
+ anim.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ dispatchRemoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setAlpha(1);
+ dispatchRemoveFinished(holder);
+ mRunningAnims.remove(anim);
+ dispatchFinishedWhenDone();
+ }
+ }
+ );
+ anim.start();
+ mRunningAnims.add(anim);
+ }
+
+ @Override
+ public boolean animateAdd(ViewHolder holder) {
+ dispatchAddFinished(holder);
+ return false;
+ }
+
+ @Override
+ public boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX,
+ int toY) {
+ dispatchMoveFinished(holder);
+ return false;
+ }
+
+ @Override
+ public boolean animateChange(ViewHolder oldHolder,
+ ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
+ // Only support changes where the holders are the same
+ if (oldHolder == newHolder) {
+ PendingAnimation pendAnim = new PendingAnimation(oldHolder, ANIM_TYPE_CHANGE);
+ mPendingAnims.add(pendAnim);
+ return true;
+ }
+ dispatchChangeFinished(oldHolder, true /* oldItem */);
+ dispatchChangeFinished(newHolder, false /* oldItem */);
+ return false;
+ }
+
+ private void animateChangeImpl(ViewHolder viewHolder, long startDelay) {
+ TaskItemView itemView = (TaskItemView) viewHolder.itemView;
+ final ObjectAnimator anim =
+ ObjectAnimator.ofFloat(itemView, CONTENT_TRANSITION_PROGRESS, 0.0f, 1.0f);
+ anim.setDuration(ITEM_CHANGE_DURATION).setStartDelay(startDelay);
+ anim.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ dispatchChangeStarting(viewHolder, true /* oldItem */);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ dispatchChangeFinished(viewHolder, true /* oldItem */);
+ mRunningAnims.remove(anim);
+ dispatchFinishedWhenDone();
+ }
+ }
+ );
+ anim.start();
+ mRunningAnims.add(anim);
+ }
+
+ @Override
+ public void runPendingAnimations() {
+ // Run animations bottom to top.
+ mPendingAnims.sort(Comparator.comparingInt(o -> -o.viewHolder.itemView.getBottom()));
+ int delay = 0;
+ while (!mPendingAnims.isEmpty()) {
+ PendingAnimation curAnim = mPendingAnims.remove(0);
+ ViewHolder vh = curAnim.viewHolder;
+ switch (curAnim.animType) {
+ case ANIM_TYPE_REMOVE:
+ animateRemoveImpl(vh, delay);
+ break;
+ case ANIM_TYPE_CHANGE:
+ animateChangeImpl(vh, delay);
+ break;
+ default:
+ break;
+ }
+ delay += ITEM_BETWEEN_DELAY;
+ }
+ }
+
+ @Override
+ public void endAnimation(@NonNull ViewHolder item) {
+ for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
+ PendingAnimation pendAnim = mPendingAnims.get(i);
+ if (pendAnim.viewHolder == item) {
+ mPendingAnims.remove(i);
+ switch (pendAnim.animType) {
+ case ANIM_TYPE_REMOVE:
+ dispatchRemoveFinished(item);
+ break;
+ case ANIM_TYPE_CHANGE:
+ dispatchChangeFinished(item, true /* oldItem */);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ dispatchFinishedWhenDone();
+ }
+
+ @Override
+ public void endAnimations() {
+ for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
+ PendingAnimation pendAnim = mPendingAnims.get(i);
+ ViewHolder item = pendAnim.viewHolder;
+ switch (pendAnim.animType) {
+ case ANIM_TYPE_REMOVE:
+ dispatchRemoveFinished(item);
+ break;
+ case ANIM_TYPE_CHANGE:
+ dispatchChangeFinished(item, true /* oldItem */);
+ break;
+ default:
+ break;
+ }
+ mPendingAnims.remove(i);
+ }
+ for (int i = 0; i < mRunningAnims.size(); i++) {
+ ObjectAnimator anim = mRunningAnims.get(i);
+ anim.end();
+ }
+ dispatchAnimationsFinished();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return !mPendingAnims.isEmpty() || !mRunningAnims.isEmpty();
+ }
+
+ @Override
+ public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+ @NonNull List payloads) {
+ if (!payloads.isEmpty()
+ && (int) payloads.get(0) == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
+ return true;
+ }
+ return super.canReuseUpdatedViewHolder(viewHolder, payloads);
+ }
+
+ private void dispatchFinishedWhenDone() {
+ if (!isRunning()) {
+ dispatchAnimationsFinished();
+ if (mOnFinishRunnable != null) {
+ mOnFinishRunnable.run();
+ }
+ }
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/TaskActionController.java b/go/quickstep/src/com/android/quickstep/TaskActionController.java
index 71bee9181d..0e921c0db1 100644
--- a/go/quickstep/src/com/android/quickstep/TaskActionController.java
+++ b/go/quickstep/src/com/android/quickstep/TaskActionController.java
@@ -15,9 +15,13 @@
*/
package com.android.quickstep;
+import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
+
import android.app.ActivityOptions;
import android.view.View;
+import androidx.annotation.NonNull;
+
import com.android.quickstep.views.TaskItemView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -37,12 +41,12 @@ public final class TaskActionController {
}
/**
- * Launch the task associated with the task holder, animating into the app.
+ * Launch the task associated with the task holder, animating into the app from the task view.
*
* @param viewHolder the task view holder to launch
*/
- public void launchTask(TaskHolder viewHolder) {
- if (viewHolder.getTask() == null) {
+ public void launchTaskFromView(@NonNull TaskHolder viewHolder) {
+ if (!viewHolder.getTask().isPresent()) {
return;
}
TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
@@ -53,8 +57,20 @@ public final class TaskActionController {
int height = v.getMeasuredHeight();
ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
- ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(viewHolder.getTask().key,
- opts, null /* resultCallback */, null /* resultCallbackHandler */);
+ ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(
+ viewHolder.getTask().get().key, opts, null /* resultCallback */,
+ null /* resultCallbackHandler */);
+ }
+
+ /**
+ * Launch the task directly with a basic animation.
+ *
+ * @param task the task to launch
+ */
+ public void launchTask(@NonNull Task task) {
+ ActivityOptions opts = ActivityOptions.makeBasic();
+ ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(task.key, opts,
+ null /* resultCallback */, null /* resultCallbackHandler */);
}
/**
@@ -63,11 +79,11 @@ public final class TaskActionController {
* @param viewHolder the task view holder to remove
*/
public void removeTask(TaskHolder viewHolder) {
- if (viewHolder.getTask() == null) {
+ if (!viewHolder.getTask().isPresent()) {
return;
}
int position = viewHolder.getAdapterPosition();
- Task task = viewHolder.getTask();
+ Task task = viewHolder.getTask().get();
ActivityManagerWrapper.getInstance().removeTask(task.key.id);
mLoader.removeTask(task);
mAdapter.notifyItemRemoved(position);
@@ -80,6 +96,6 @@ public final class TaskActionController {
int count = mAdapter.getItemCount();
ActivityManagerWrapper.getInstance().removeAllRecentTasks();
mLoader.clearAllTasks();
- mAdapter.notifyItemRangeRemoved(0 /* positionStart */, count);
+ mAdapter.notifyItemRangeRemoved(TASKS_START_POSITION /* positionStart */, count);
}
}
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
index 674fcae914..509bf29a4e 100644
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
@@ -15,13 +15,15 @@
*/
package com.android.quickstep;
-import android.util.ArrayMap;
import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
+import android.widget.Button;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.launcher3.R;
import com.android.quickstep.views.TaskItemView;
@@ -29,18 +31,25 @@ import com.android.systemui.shared.recents.model.Task;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
/**
* Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the
* appropriate {@link Task} from the recents task list.
*/
-public final class TaskAdapter extends Adapter {
+public final class TaskAdapter extends Adapter {
+
+ public static final int CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT = 0;
+ public static final int MAX_TASKS_TO_DISPLAY = 6;
+ public static final int TASKS_START_POSITION = 1;
+
+ public static final int ITEM_TYPE_TASK = 0;
+ public static final int ITEM_TYPE_CLEAR_ALL = 1;
- private static final int MAX_TASKS_TO_DISPLAY = 6;
private static final String TAG = "TaskAdapter";
private final TaskListLoader mLoader;
- private final ArrayMap mTaskIdToViewMap = new ArrayMap<>();
private TaskActionController mTaskActionController;
+ private OnClickListener mClearAllListener;
private boolean mIsShowingLoadingUi;
public TaskAdapter(@NonNull TaskListLoader loader) {
@@ -51,6 +60,10 @@ public final class TaskAdapter extends Adapter {
mTaskActionController = taskActionController;
}
+ public void setOnClearAllClickListener(OnClickListener listener) {
+ mClearAllListener = listener;
+ }
+
/**
* Sets all positions in the task adapter to loading views, binding new views if necessary.
* This changes the task adapter's view of the data, so the appropriate notify events should be
@@ -63,75 +76,104 @@ public final class TaskAdapter extends Adapter {
mIsShowingLoadingUi = isShowingLoadingUi;
}
- /**
- * Get task item view for a given task id if it's attached to the view.
- *
- * @param taskId task id to search for
- * @return corresponding task item view if it's attached, null otherwise
- */
- public @Nullable TaskItemView getTaskItemView(int taskId) {
- return mTaskIdToViewMap.get(taskId);
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case ITEM_TYPE_TASK:
+ TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.task_item_view, parent, false);
+ TaskHolder taskHolder = new TaskHolder(itemView);
+ itemView.setOnClickListener(
+ view -> mTaskActionController.launchTaskFromView(taskHolder));
+ return taskHolder;
+ case ITEM_TYPE_CLEAR_ALL:
+ View clearView = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.clear_all_button, parent, false);
+ ClearAllHolder clearAllHolder = new ClearAllHolder(clearView);
+ Button clearViewButton = clearView.findViewById(R.id.clear_all_button);
+ clearViewButton.setOnClickListener(mClearAllListener);
+ return clearAllHolder;
+ default:
+ throw new IllegalArgumentException("No known holder for item type: " + viewType);
+ }
}
@Override
- public TaskHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
- .inflate(R.layout.task_item_view, parent, false);
- TaskHolder holder = new TaskHolder(itemView);
- itemView.setOnClickListener(view -> mTaskActionController.launchTask(holder));
- return holder;
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ onBindViewHolderInternal(holder, position, false /* willAnimate */);
}
@Override
- public void onBindViewHolder(TaskHolder holder, int position) {
- if (mIsShowingLoadingUi) {
- holder.bindEmptyUi();
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position,
+ @NonNull List payloads) {
+ if (payloads.isEmpty()) {
+ super.onBindViewHolder(holder, position, payloads);
return;
}
- List tasks = mLoader.getCurrentTaskList();
- if (position >= tasks.size()) {
- // Task list has updated.
- return;
+ int changeType = (int) payloads.get(0);
+ if (changeType == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
+ // Bind in preparation for animation
+ onBindViewHolderInternal(holder, position, true /* willAnimate */);
+ } else {
+ throw new IllegalArgumentException("Payload content is not a valid change event type: "
+ + changeType);
+ }
+ }
+
+ private void onBindViewHolderInternal(@NonNull ViewHolder holder, int position,
+ boolean willAnimate) {
+ int itemType = getItemViewType(position);
+ switch (itemType) {
+ case ITEM_TYPE_TASK:
+ TaskHolder taskHolder = (TaskHolder) holder;
+ if (mIsShowingLoadingUi) {
+ taskHolder.bindEmptyUi();
+ return;
+ }
+ List tasks = mLoader.getCurrentTaskList();
+ int taskPos = position - TASKS_START_POSITION;
+ if (taskPos >= tasks.size()) {
+ // Task list has updated.
+ return;
+ }
+ Task task = tasks.get(taskPos);
+ taskHolder.bindTask(task, willAnimate /* willAnimate */);
+ mLoader.loadTaskIconAndLabel(task, () -> {
+ // Ensure holder still has the same task.
+ if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
+ taskHolder.getTaskItemView().setIcon(task.icon);
+ taskHolder.getTaskItemView().setLabel(task.titleDescription);
+ }
+ });
+ mLoader.loadTaskThumbnail(task, () -> {
+ if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
+ taskHolder.getTaskItemView().setThumbnail(task.thumbnail);
+ }
+ });
+ break;
+ case ITEM_TYPE_CLEAR_ALL:
+ // Nothing to bind.
+ break;
+ default:
+ throw new IllegalArgumentException("No known holder for item type: " + itemType);
}
- Task task = tasks.get(position);
- holder.bindTask(task);
- mLoader.loadTaskIconAndLabel(task, () -> {
- // Ensure holder still has the same task.
- if (Objects.equals(task, holder.getTask())) {
- holder.getTaskItemView().setIcon(task.icon);
- holder.getTaskItemView().setLabel(task.titleDescription);
- }
- });
- mLoader.loadTaskThumbnail(task, () -> {
- if (Objects.equals(task, holder.getTask())) {
- holder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
- }
- });
}
@Override
- public void onViewAttachedToWindow(@NonNull TaskHolder holder) {
- if (holder.getTask() == null) {
- return;
- }
- mTaskIdToViewMap.put(holder.getTask().key.id, (TaskItemView) holder.itemView);
- }
-
- @Override
- public void onViewDetachedFromWindow(@NonNull TaskHolder holder) {
- if (holder.getTask() == null) {
- return;
- }
- mTaskIdToViewMap.remove(holder.getTask().key.id);
+ public int getItemViewType(int position) {
+ // Bottom is always clear all button.
+ return (position == 0) ? ITEM_TYPE_CLEAR_ALL : ITEM_TYPE_TASK;
}
@Override
public int getItemCount() {
+ int itemCount = TASKS_START_POSITION;
if (mIsShowingLoadingUi) {
// Show loading version of all items.
- return MAX_TASKS_TO_DISPLAY;
+ itemCount += MAX_TASKS_TO_DISPLAY;
} else {
- return Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
+ itemCount += Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
}
+ return itemCount;
}
}
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
index 98dc989317..49b6aaa996 100644
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ b/go/quickstep/src/com/android/quickstep/TaskHolder.java
@@ -15,12 +15,14 @@
*/
package com.android.quickstep;
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.quickstep.views.TaskItemView;
import com.android.systemui.shared.recents.model.Task;
+import java.util.Optional;
+
/**
* A recycler view holder that holds the task view and binds {@link Task} content (app title, icon,
* etc.) to the view.
@@ -40,13 +42,27 @@ public final class TaskHolder extends ViewHolder {
}
/**
- * Bind a task to the holder, resetting the view and preparing it for content to load in.
+ * Bind the task model to the holder. This will take the current task content in the task
+ * object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
+ * the content to animate to at a later time. If the task does not have all its content loaded,
+ * the view will prepare appropriate default placeholders and it is the callers responsibility
+ * to change them at a later time.
+ *
+ * Regardless of whether it is animating, input handlers will be bound immediately (see
+ * {@link TaskActionController}).
*
* @param task the task to bind to the view
+ * @param willAnimate true if UI should animate in later, false if it should apply immediately
*/
- public void bindTask(Task task) {
+ public void bindTask(@NonNull Task task, boolean willAnimate) {
mTask = task;
- mTaskItemView.resetTaskItemView();
+ if (willAnimate) {
+ mTaskItemView.startContentAnimation(task.icon, task.thumbnail, task.titleDescription);
+ } else {
+ mTaskItemView.setIcon(task.icon);
+ mTaskItemView.setThumbnail(task.thumbnail);
+ mTaskItemView.setLabel(task.titleDescription);
+ }
}
/**
@@ -55,10 +71,7 @@ public final class TaskHolder extends ViewHolder {
*/
public void bindEmptyUi() {
mTask = null;
- // TODO: Set the task view to a loading, empty UI.
- // Temporarily using the one below for visual confirmation but should be swapped out to new
- // UI later.
- mTaskItemView.resetTaskItemView();
+ mTaskItemView.resetToEmptyUi();
}
/**
@@ -66,7 +79,7 @@ public final class TaskHolder extends ViewHolder {
*
* @return the current task
*/
- public @Nullable Task getTask() {
- return mTask;
+ public Optional getTask() {
+ return Optional.ofNullable(mTask);
}
}
diff --git a/go/quickstep/src/com/android/quickstep/TaskListLoader.java b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
index 51b73f1815..850c7e68a4 100644
--- a/go/quickstep/src/com/android/quickstep/TaskListLoader.java
+++ b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
@@ -38,23 +38,9 @@ public final class TaskListLoader {
private ArrayList 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);
}
/**
diff --git a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java b/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
index 98407d8b74..19951bb378 100644
--- a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
+++ b/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
@@ -17,6 +17,9 @@ package com.android.quickstep;
import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
+import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
+
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
@@ -45,4 +48,14 @@ public final class TaskSwipeCallback extends ItemTouchHelper.SimpleCallback {
mTaskActionController.removeTask((TaskHolder) viewHolder);
}
}
+
+ @Override
+ public int getSwipeDirs(@NonNull RecyclerView recyclerView,
+ @NonNull ViewHolder viewHolder) {
+ if (viewHolder.getItemViewType() == ITEM_TYPE_CLEAR_ALL) {
+ // Clear all button should not be swipable.
+ return 0;
+ }
+ return super.getSwipeDirs(recyclerView, viewHolder);
+ }
}
diff --git a/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java b/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
new file mode 100644
index 0000000000..6ef9039f68
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2019 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.quickstep;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * Bitmap backed drawable that supports rotating the thumbnail bitmap depending on if the
+ * orientation the thumbnail was taken in matches the desired orientation. In addition, the
+ * thumbnail always fills into the containing bounds.
+ */
+public final class ThumbnailDrawable extends Drawable {
+
+ private final Paint mPaint = new Paint();
+ private final Matrix mMatrix = new Matrix();
+ private final ThumbnailData mThumbnailData;
+ private int mRequestedOrientation;
+
+ public ThumbnailDrawable(@NonNull ThumbnailData thumbnailData, int requestedOrientation) {
+ mThumbnailData = thumbnailData;
+ mRequestedOrientation = requestedOrientation;
+ updateMatrix();
+ }
+
+ /**
+ * Set the requested orientation.
+ *
+ * @param orientation the orientation we want the thumbnail to be in
+ */
+ public void setRequestedOrientation(int orientation) {
+ if (mRequestedOrientation != orientation) {
+ mRequestedOrientation = orientation;
+ updateMatrix();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mThumbnailData.thumbnail == null) {
+ return;
+ }
+ canvas.drawBitmap(mThumbnailData.thumbnail, mMatrix, mPaint);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ updateMatrix();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ final int oldAlpha = mPaint.getAlpha();
+ if (alpha != oldAlpha) {
+ mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return mPaint.getAlpha();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ invalidateSelf();
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mPaint.getColorFilter();
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ private void updateMatrix() {
+ if (mThumbnailData.thumbnail == null) {
+ return;
+ }
+ mMatrix.reset();
+ float scaleX;
+ float scaleY;
+ Rect bounds = getBounds();
+ Bitmap thumbnail = mThumbnailData.thumbnail;
+ if (mRequestedOrientation != mThumbnailData.orientation) {
+ // Rotate and translate so that top left is the same.
+ mMatrix.postRotate(90, 0, 0);
+ mMatrix.postTranslate(thumbnail.getHeight(), 0);
+
+ scaleX = (float) bounds.width() / thumbnail.getHeight();
+ scaleY = (float) bounds.height() / thumbnail.getWidth();
+ } else {
+ scaleX = (float) bounds.width() / thumbnail.getWidth();
+ scaleY = (float) bounds.height() / thumbnail.getHeight();
+ }
+ // Scale to fill.
+ mMatrix.postScale(scaleX, scaleY);
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java b/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java
new file mode 100644
index 0000000000..378dbf4e4a
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.quickstep.views;
+
+import static com.android.quickstep.views.TaskLayoutUtils.getClearAllItemHeight;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Recycler view item that lays out the clear all button and measures the space it takes based on
+ * the device height.
+ */
+public final class ClearAllItemView extends FrameLayout {
+
+ public ClearAllItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int buttonHeight = getClearAllItemHeight(getContext());
+ int newHeightSpec = MeasureSpec.makeMeasureSpec(buttonHeight, MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, newHeightSpec);
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index c06b6ec416..cf6eb6d33d 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -15,8 +15,13 @@
*/
package com.android.quickstep.views;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
+import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
+import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -24,30 +29,39 @@ import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
+import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.View;
import android.view.ViewDebug;
-import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
-import android.view.animation.AnimationSet;
-import android.view.animation.LayoutAnimationController;
+import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
+import com.android.launcher3.BaseActivity;
import com.android.launcher3.R;
+import com.android.quickstep.ContentFillItemAnimator;
+import com.android.quickstep.RecentsModel;
import com.android.quickstep.RecentsToActivityHelper;
import com.android.quickstep.TaskActionController;
import com.android.quickstep.TaskAdapter;
import com.android.quickstep.TaskHolder;
import com.android.quickstep.TaskListLoader;
import com.android.quickstep.TaskSwipeCallback;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
/**
* Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
@@ -89,24 +103,48 @@ public final class IconRecentsView extends FrameLayout {
private final Context mContext;
private final TaskListLoader mTaskLoader;
private final TaskAdapter mTaskAdapter;
+ private final LinearLayoutManager mTaskLayoutManager;
private final TaskActionController mTaskActionController;
- private final LayoutAnimationController mLayoutAnimation;
+ private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
+ private final ContentFillItemAnimator mLoadingContentItemAnimator =
+ new ContentFillItemAnimator();
private RecentsToActivityHelper mActivityHelper;
private RecyclerView mTaskRecyclerView;
+ private View mShowingContentView;
private View mEmptyView;
private View mContentView;
- private View mClearAllView;
private boolean mTransitionedFromApp;
+ private AnimatorSet mLayoutAnimation;
+ private final ArraySet mLayingOutViews = new ArraySet<>();
+ private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
+ ArrayList itemViews = getTaskViews();
+ for (int i = 0, size = itemViews.size(); i < size; i++) {
+ TaskItemView taskView = itemViews.get(i);
+ TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
+ Optional optTask = taskHolder.getTask();
+ if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
+ Task task = optTask.get();
+ // Update thumbnail on the task.
+ task.thumbnail = thumbnailData;
+ taskView.setThumbnail(thumbnailData);
+ return task;
+ }
+ }
+ return null;
+ };
public IconRecentsView(Context context, AttributeSet attrs) {
super(context, attrs);
+ BaseActivity activity = BaseActivity.fromContext(context);
mContext = context;
mTaskLoader = new TaskListLoader(mContext);
mTaskAdapter = new TaskAdapter(mTaskLoader);
+ mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
mTaskAdapter.setActionController(mTaskActionController);
- mLayoutAnimation = createLayoutAnimation();
+ mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
+ RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
}
@Override
@@ -115,15 +153,30 @@ public final class IconRecentsView extends FrameLayout {
if (mTaskRecyclerView == null) {
mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
mTaskRecyclerView.setAdapter(mTaskAdapter);
- mTaskRecyclerView.setLayoutManager(
- new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */));
+ mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
ItemTouchHelper helper = new ItemTouchHelper(
new TaskSwipeCallback(mTaskActionController));
helper.attachToRecyclerView(mTaskRecyclerView);
- mTaskRecyclerView.setLayoutAnimation(mLayoutAnimation);
+ mTaskRecyclerView.addOnChildAttachStateChangeListener(
+ new OnChildAttachStateChangeListener() {
+ @Override
+ public void onChildViewAttachedToWindow(@NonNull View view) {
+ if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
+ // Child view was added that is not part of current layout animation
+ // so restart the animation.
+ animateFadeInLayoutAnimation();
+ }
+ }
+
+ @Override
+ public void onChildViewDetachedFromWindow(@NonNull View view) { }
+ });
+ mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
+ mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
+ () -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
mEmptyView = findViewById(R.id.recent_task_empty_view);
- mContentView = findViewById(R.id.recent_task_content_view);
+ mContentView = mTaskRecyclerView;
mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onChanged() {
@@ -135,19 +188,17 @@ public final class IconRecentsView extends FrameLayout {
updateContentViewVisibility();
}
});
- mClearAllView = findViewById(R.id.clear_all_button);
- mClearAllView.setOnClickListener(v -> animateClearAllTasks());
+ // TODO: Move layout param logic into onMeasure
}
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
- TaskItemView[] itemViews = getTaskViews();
- for (TaskItemView itemView : itemViews) {
- itemView.setEnabled(enabled);
+ int childCount = mTaskRecyclerView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
}
- mClearAllView.setEnabled(enabled);
}
/**
@@ -165,8 +216,13 @@ public final class IconRecentsView extends FrameLayout {
* becomes visible.
*/
public void onBeginTransitionToOverview() {
- mTaskRecyclerView.scheduleLayoutAnimation();
-
+ if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
+ // Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
+ // all tasks should be visible to fill up the screen in portrait mode and the view will
+ // not be scrollable.
+ mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
+ }
+ scheduleFadeInLayoutAnimation();
// Load any task changes
if (!mTaskLoader.needsToLoad()) {
return;
@@ -174,9 +230,24 @@ public final class IconRecentsView extends FrameLayout {
mTaskAdapter.setIsShowingLoadingUi(true);
mTaskAdapter.notifyDataSetChanged();
mTaskLoader.loadTaskList(tasks -> {
+ int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
mTaskAdapter.setIsShowingLoadingUi(false);
- // TODO: Animate the loading UI out and the loaded data in.
- mTaskAdapter.notifyDataSetChanged();
+ int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
+ if (numEmptyItems < numActualItems) {
+ throw new IllegalStateException("There are less empty item views than the number "
+ + "of items to animate to.");
+ }
+ // Possible that task list loads faster than adapter changes propagate to layout so
+ // only start content fill animation if there aren't any pending adapter changes.
+ if (!mTaskRecyclerView.hasPendingAdapterUpdates()) {
+ // Set item animator for content filling animation. The item animator will switch
+ // back to the default on completion
+ mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
+ }
+ mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
+ numEmptyItems - numActualItems);
+ mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
+ CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
});
}
@@ -194,35 +265,47 @@ public final class IconRecentsView extends FrameLayout {
* the app. In that case, we launch the next most recent.
*/
public void handleOverviewCommand() {
- int childCount = mTaskRecyclerView.getChildCount();
- if (childCount == 0) {
+ List tasks = mTaskLoader.getCurrentTaskList();
+ int tasksSize = tasks.size();
+ if (tasksSize == 0) {
// Do nothing
return;
}
- TaskHolder taskToLaunch;
- if (mTransitionedFromApp && childCount > 1) {
+ Task taskToLaunch;
+ if (mTransitionedFromApp && tasksSize > 1) {
// Launch the next most recent app
- TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(1);
- taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
+ taskToLaunch = tasks.get(1);
} else {
// Launch the most recent app
- TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(0);
- taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
+ taskToLaunch = tasks.get(0);
}
+
+ // See if view for this task is attached, and if so, animate launch from that view.
+ ArrayList itemViews = getTaskViews();
+ for (int i = 0, size = itemViews.size(); i < size; i++) {
+ TaskItemView taskView = itemViews.get(i);
+ TaskHolder holder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
+ if (Objects.equals(holder.getTask(), Optional.of(taskToLaunch))) {
+ mTaskActionController.launchTaskFromView(holder);
+ return;
+ }
+ }
+
+ // Otherwise, just use a basic launch animation.
mTaskActionController.launchTask(taskToLaunch);
}
/**
- * Get the thumbnail view associated with a task for the purposes of animation.
+ * Get the bottom most thumbnail view to animate to.
*
- * @param taskId task id of thumbnail view to get
- * @return the thumbnail view for the task if attached, null otherwise
+ * @return the thumbnail view if laid out
*/
- public @Nullable View getThumbnailViewForTask(int taskId) {
- TaskItemView view = mTaskAdapter.getTaskItemView(taskId);
- if (view == null) {
+ public @Nullable View getBottomThumbnailView() {
+ ArrayList taskViews = getTaskViews();
+ if (taskViews.isEmpty()) {
return null;
}
+ TaskItemView view = taskViews.get(0);
return view.getThumbnailView();
}
@@ -231,13 +314,14 @@ public final class IconRecentsView extends FrameLayout {
*/
private void animateClearAllTasks() {
setEnabled(false);
- TaskItemView[] itemViews = getTaskViews();
+ ArrayList itemViews = getTaskViews();
AnimatorSet clearAnim = new AnimatorSet();
long currentDelay = 0;
// Animate each item view to the right and fade out.
- for (TaskItemView itemView : itemViews) {
+ for (int i = 0, size = itemViews.size(); i < size; i++) {
+ TaskItemView itemView = itemViews.get(i);
PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
@@ -272,7 +356,8 @@ public final class IconRecentsView extends FrameLayout {
clearAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- for (TaskItemView itemView : itemViews) {
+ for (int i = 0, size = itemViews.size(); i < size; i++) {
+ TaskItemView itemView = itemViews.get(i);
itemView.setTranslationX(0);
itemView.setAlpha(1.0f);
}
@@ -287,13 +372,16 @@ public final class IconRecentsView extends FrameLayout {
/**
* Get attached task item views ordered by most recent.
*
- * @return array of attached task item views
+ * @return array list of attached task item views
*/
- private TaskItemView[] getTaskViews() {
+ private ArrayList getTaskViews() {
int taskCount = mTaskRecyclerView.getChildCount();
- TaskItemView[] itemViews = new TaskItemView[taskCount];
+ ArrayList itemViews = new ArrayList<>();
for (int i = 0; i < taskCount; i ++) {
- itemViews[i] = (TaskItemView) mTaskRecyclerView.getChildAt(i);
+ View child = mTaskRecyclerView.getChildAt(i);
+ if (child instanceof TaskItemView) {
+ itemViews.add((TaskItemView) child);
+ }
}
return itemViews;
}
@@ -303,12 +391,14 @@ public final class IconRecentsView extends FrameLayout {
* of tasks.
*/
private void updateContentViewVisibility() {
- int taskListSize = mTaskLoader.getCurrentTaskList().size();
- if (mEmptyView.getVisibility() != VISIBLE && taskListSize == 0) {
+ int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
+ if (mShowingContentView != mEmptyView && taskListSize == 0) {
+ mShowingContentView = mEmptyView;
crossfadeViews(mEmptyView, mContentView);
mActivityHelper.leaveRecents();
}
- if (mContentView.getVisibility() != VISIBLE && taskListSize > 0) {
+ if (mShowingContentView != mContentView && taskListSize > 0) {
+ mShowingContentView = mContentView;
crossfadeViews(mContentView, mEmptyView);
}
}
@@ -320,6 +410,7 @@ public final class IconRecentsView extends FrameLayout {
* @param fadeOutView view that should fade out
*/
private void crossfadeViews(View fadeInView, View fadeOutView) {
+ fadeInView.animate().cancel();
fadeInView.setVisibility(VISIBLE);
fadeInView.setAlpha(0f);
fadeInView.animate()
@@ -327,6 +418,7 @@ public final class IconRecentsView extends FrameLayout {
.setDuration(CROSSFADE_DURATION)
.setListener(null);
+ fadeOutView.animate().cancel();
fadeOutView.animate()
.alpha(0f)
.setDuration(CROSSFADE_DURATION)
@@ -338,17 +430,56 @@ public final class IconRecentsView extends FrameLayout {
});
}
- private static LayoutAnimationController createLayoutAnimation() {
- AnimationSet anim = new AnimationSet(false /* shareInterpolator */);
+ /**
+ * Schedule a one-shot layout animation on the next layout. Separate from
+ * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
+ * view properties themselves, allowing more controllable behavior and making it easier to
+ * manage when the animation conflicts with another animation.
+ */
+ private void scheduleFadeInLayoutAnimation() {
+ mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ animateFadeInLayoutAnimation();
+ mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ });
+ }
- Animation alphaAnim = new AlphaAnimation(0, 1);
- alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION);
- anim.addAnimation(alphaAnim);
-
- LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
- layoutAnim.setDelay(
- (float) LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN / LAYOUT_ITEM_ANIMATE_IN_DURATION);
-
- return layoutAnim;
+ /**
+ * Start animating the layout animation where items fade in.
+ */
+ private void animateFadeInLayoutAnimation() {
+ if (mLayoutAnimation != null) {
+ // If layout animation still in progress, cancel and restart.
+ mLayoutAnimation.cancel();
+ }
+ ArrayList views = getTaskViews();
+ int delay = 0;
+ mLayoutAnimation = new AnimatorSet();
+ for (int i = 0, size = views.size(); i < size; i++) {
+ TaskItemView view = views.get(i);
+ view.setAlpha(0.0f);
+ Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
+ alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
+ alphaAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setAlpha(1.0f);
+ mLayingOutViews.remove(view);
+ }
+ });
+ delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
+ mLayoutAnimation.play(alphaAnim);
+ mLayingOutViews.add(view);
+ }
+ mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mLayoutAnimation = null;
+ }
+ });
+ mLayoutAnimation.start();
}
}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
index d831b206a6..0b5ed569c8 100644
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
@@ -15,35 +15,66 @@
*/
package com.android.quickstep.views;
+import static com.android.quickstep.views.TaskLayoutUtils.getTaskHeight;
+
import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
+import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
+import android.util.FloatProperty;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
+import com.android.quickstep.ThumbnailDrawable;
+import com.android.systemui.shared.recents.model.ThumbnailData;
/**
* View representing an individual task item with the icon + thumbnail adjacent to the task label.
*/
public final class TaskItemView extends LinearLayout {
+ private static final String EMPTY_LABEL = "";
private static final String DEFAULT_LABEL = "...";
private final Drawable mDefaultIcon;
+ private final Drawable mDefaultThumbnail;
+ private final TaskLayerDrawable mIconDrawable;
+ private final TaskLayerDrawable mThumbnailDrawable;
private TextView mLabelView;
private ImageView mIconView;
private ImageView mThumbnailView;
+ private float mContentTransitionProgress;
+
+ /**
+ * Property representing the content transition progress of the view. 1.0f represents that the
+ * currently bound icon, thumbnail, and label are fully animated in and visible.
+ */
+ public static FloatProperty CONTENT_TRANSITION_PROGRESS =
+ new FloatProperty("taskContentTransitionProgress") {
+ @Override
+ public void setValue(TaskItemView view, float progress) {
+ view.setContentTransitionProgress(progress);
+ }
+
+ @Override
+ public Float get(TaskItemView view) {
+ return view.mContentTransitionProgress;
+ }
+ };
public TaskItemView(Context context, AttributeSet attrs) {
super(context, attrs);
- mDefaultIcon = context.getResources().getDrawable(
- android.R.drawable.sym_def_app_icon, context.getTheme());
+ Resources res = context.getResources();
+ mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
+ mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
+ mIconDrawable = new TaskLayerDrawable(context);
+ mThumbnailDrawable = new TaskLayerDrawable(context);
}
@Override
@@ -52,15 +83,28 @@ public final class TaskItemView extends LinearLayout {
mLabelView = findViewById(R.id.task_label);
mThumbnailView = findViewById(R.id.task_thumbnail);
mIconView = findViewById(R.id.task_icon);
+
+ mThumbnailView.setImageDrawable(mThumbnailDrawable);
+ mIconView.setImageDrawable(mIconDrawable);
+
+ resetToEmptyUi();
+ CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int taskHeight = getTaskHeight(getContext());
+ int newHeightSpec = MeasureSpec.makeMeasureSpec(taskHeight,MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, newHeightSpec);
}
/**
- * Resets task item view to default values.
+ * Resets task item view to empty, loading UI.
*/
- public void resetTaskItemView() {
- setLabel(DEFAULT_LABEL);
- setIcon(null);
- setThumbnail(null);
+ public void resetToEmptyUi() {
+ mIconDrawable.resetDrawable();
+ mThumbnailDrawable.resetDrawable();
+ setLabel(EMPTY_LABEL);
}
/**
@@ -69,11 +113,8 @@ public final class TaskItemView extends LinearLayout {
* @param label task label
*/
public void setLabel(@Nullable String label) {
- if (label == null) {
- mLabelView.setText(DEFAULT_LABEL);
- return;
- }
- mLabelView.setText(label);
+ mLabelView.setText(getSafeLabel(label));
+ // TODO: Animation for label
}
/**
@@ -86,29 +127,72 @@ public final class TaskItemView extends LinearLayout {
// 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);
+ mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
}
/**
* Set the task thumbnail for the task. Sets to a default thumbnail if null.
*
- * @param thumbnail task thumbnail for the task
+ * @param thumbnailData task thumbnail data for the task
*/
- public void setThumbnail(@Nullable Bitmap thumbnail) {
- if (thumbnail == null) {
- mThumbnailView.setImageBitmap(null);
- mThumbnailView.setBackgroundColor(Color.GRAY);
- return;
- }
- mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
- mThumbnailView.setImageBitmap(thumbnail);
+ public void setThumbnail(@Nullable ThumbnailData thumbnailData) {
+ mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnailData));
}
public View getThumbnailView() {
return mThumbnailView;
}
+
+ /**
+ * Start a new animation from the current task content to the specified new content. The caller
+ * is responsible for the actual animation control via the property
+ * {@link #CONTENT_TRANSITION_PROGRESS}.
+ *
+ * @param endIcon the icon to animate to
+ * @param endThumbnail the thumbnail to animate to
+ * @param endLabel the label to animate to
+ */
+ public void startContentAnimation(@Nullable Drawable endIcon,
+ @Nullable ThumbnailData endThumbnail, @Nullable String endLabel) {
+ mIconDrawable.startNewTransition(getSafeIcon(endIcon));
+ mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
+ // TODO: Animation for label
+
+ setContentTransitionProgress(0.0f);
+ }
+
+ private void setContentTransitionProgress(float progress) {
+ mContentTransitionProgress = progress;
+ mIconDrawable.setTransitionProgress(progress);
+ mThumbnailDrawable.setTransitionProgress(progress);
+ // TODO: Animation for label
+ }
+
+ private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
+ return (icon != null) ? icon : mDefaultIcon;
+ }
+
+ private @NonNull Drawable getSafeThumbnail(@Nullable ThumbnailData thumbnailData) {
+ if (thumbnailData == null || thumbnailData.thumbnail == null) {
+ return mDefaultThumbnail;
+ }
+ int orientation = getResources().getConfiguration().orientation;
+ return new ThumbnailDrawable(thumbnailData, orientation /* requestedOrientation */);
+ }
+
+ private @NonNull String getSafeLabel(@Nullable String label) {
+ return (label != null) ? label : DEFAULT_LABEL;
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ int layerCount = mThumbnailDrawable.getNumberOfLayers();
+ for (int i = 0; i < layerCount; i++) {
+ Drawable drawable = mThumbnailDrawable.getDrawable(i);
+ if (drawable instanceof ThumbnailDrawable) {
+ ((ThumbnailDrawable) drawable).setRequestedOrientation(newConfig.orientation);
+ }
+ }
+ }
}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
new file mode 100644
index 0000000000..98b66b995d
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 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.quickstep.views;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.R;
+
+/**
+ * A layer drawable for task content that transitions between two drawables by crossfading. Similar
+ * to {@link android.graphics.drawable.TransitionDrawable} but allows callers to control transition
+ * progress and provides a default, empty drawable.
+ */
+public final class TaskLayerDrawable extends LayerDrawable {
+ private final Drawable mEmptyDrawable;
+ private float mProgress;
+
+ public TaskLayerDrawable(Context context) {
+ super(new Drawable[0]);
+
+ // Use empty drawable for both layers initially.
+ mEmptyDrawable = context.getResources().getDrawable(
+ R.drawable.empty_content_box, context.getTheme());
+ addLayer(mEmptyDrawable);
+ addLayer(mEmptyDrawable);
+ setTransitionProgress(1.0f);
+ }
+
+ /**
+ * Immediately set the front-most drawable layer.
+ *
+ * @param drawable drawable to set
+ */
+ public void setCurrentDrawable(@NonNull Drawable drawable) {
+ setDrawable(0, drawable);
+ applyTransitionProgress(mProgress);
+ }
+
+ /**
+ * Immediately reset the drawable to showing the empty drawable.
+ */
+ public void resetDrawable() {
+ setCurrentDrawable(mEmptyDrawable);
+ }
+
+ /**
+ * Prepare to start animating the transition by pushing the current drawable to the back and
+ * setting a new drawable to the front layer and making it invisible.
+ *
+ * @param endDrawable drawable to animate to
+ */
+ public void startNewTransition(@NonNull Drawable endDrawable) {
+ Drawable oldDrawable = getDrawable(0);
+ setDrawable(1, oldDrawable);
+ setDrawable(0, endDrawable);
+ setTransitionProgress(0.0f);
+ }
+
+ /**
+ * Set the progress of the transition animation to crossfade the two drawables.
+ *
+ * @param progress current transition progress between 0 (front view invisible) and 1
+ * (front view visible)
+ */
+ public void setTransitionProgress(float progress) {
+ if (progress > 1 || progress < 0) {
+ throw new IllegalArgumentException("Transition progress should be between 0 and 1");
+ }
+ mProgress = progress;
+ applyTransitionProgress(progress);
+ }
+
+ private void applyTransitionProgress(float progress) {
+ int drawableAlpha = (int) (progress * 255);
+ getDrawable(0).setAlpha(drawableAlpha);
+ if (getDrawable(0) != getDrawable(1)) {
+ // Only do this if it's a different drawable so that it fades out.
+ // Otherwise, we'd just be overwriting the front drawable's alpha.
+ getDrawable(1).setAlpha(255 - drawableAlpha);
+ }
+ invalidateSelf();
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java b/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java
new file mode 100644
index 0000000000..e28a9e04f0
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 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.quickstep.views;
+
+import static com.android.quickstep.TaskAdapter.MAX_TASKS_TO_DISPLAY;
+
+import android.content.Context;
+
+import com.android.launcher3.InvariantDeviceProfile;
+
+/**
+ * Utils to determine dynamically task and view sizes based off the device height and width.
+ */
+public final class TaskLayoutUtils {
+
+ private static final float CLEAR_ALL_ITEM_TO_HEIGHT_RATIO = 7.0f / 64;
+
+ private TaskLayoutUtils() {}
+
+ /**
+ * Calculate task height based off the available height in portrait mode such that when the
+ * recents list is full, the total height fills in the available device height perfectly. In
+ * landscape mode, we keep the same task height so that tasks scroll off the top.
+ *
+ * @param context current context
+ * @return task height
+ */
+ public static int getTaskHeight(Context context) {
+ final int availableHeight =
+ InvariantDeviceProfile.INSTANCE.get(context).portraitProfile.availableHeightPx;
+ final int availableTaskSpace = availableHeight - getClearAllItemHeight(context);
+ return (int) (availableTaskSpace * 1.0f / MAX_TASKS_TO_DISPLAY);
+ }
+
+ /**
+ * Calculate clear all item height scaled to available height in portrait mode.
+ *
+ * @param context current context
+ * @return clear all item height
+ */
+ public static int getClearAllItemHeight(Context context) {
+ final int availableHeight =
+ InvariantDeviceProfile.INSTANCE.get(context).portraitProfile.availableHeightPx;
+ return (int) (CLEAR_ALL_ITEM_TO_HEIGHT_RATIO * availableHeight);
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java b/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
new file mode 100644
index 0000000000..b1c60dd978
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2019 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.quickstep.views;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+
+/**
+ * Square view that holds thumbnail and icon and shrinks them appropriately so that both fit nicely
+ * within the view. Side length is determined by height.
+ */
+final class TaskThumbnailIconView extends ViewGroup {
+ private final Rect mTmpFrameRect = new Rect();
+ private final Rect mTmpChildRect = new Rect();
+ private View mThumbnailView;
+ private View mIconView;
+ private static final float SUBITEM_FRAME_RATIO = .6f;
+
+ public TaskThumbnailIconView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mThumbnailView = findViewById(R.id.task_thumbnail);
+ mIconView = findViewById(R.id.task_icon);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
+ int width = height;
+ setMeasuredDimension(width, height);
+
+ int subItemSize = (int) (SUBITEM_FRAME_RATIO * height);
+ if (mThumbnailView.getVisibility() != GONE) {
+ int thumbnailHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ int thumbnailWidthSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+ measureChild(mThumbnailView, thumbnailWidthSpec, thumbnailHeightSpec);
+ }
+ if (mIconView.getVisibility() != GONE) {
+ int iconHeightSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+ int iconWidthSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+ measureChild(mIconView, iconWidthSpec, iconHeightSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mTmpFrameRect.left = getPaddingLeft();
+ mTmpFrameRect.right = right - left - getPaddingRight();
+ mTmpFrameRect.top = getPaddingTop();
+ mTmpFrameRect.bottom = bottom - top - getPaddingBottom();
+
+ // Layout the thumbnail to the top-start corner of the view
+ if (mThumbnailView.getVisibility() != GONE) {
+ final int width = mThumbnailView.getMeasuredWidth();
+ final int height = mThumbnailView.getMeasuredHeight();
+
+ final int thumbnailGravity = Gravity.TOP | Gravity.START;
+ Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
+
+ mThumbnailView.layout(mTmpChildRect.left, mTmpChildRect.top,
+ mTmpChildRect.right, mTmpChildRect.bottom);
+ }
+
+ // Layout the icon to the bottom-end corner of the view
+ if (mIconView.getVisibility() != GONE) {
+ final int width = mIconView.getMeasuredWidth();
+ final int height = mIconView.getMeasuredHeight();
+
+ int thumbnailGravity = Gravity.BOTTOM | Gravity.END;
+ Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
+
+ mIconView.layout(mTmpChildRect.left, mTmpChildRect.top,
+ mTmpChildRect.right, mTmpChildRect.bottom);
+ }
+ }
+}
diff --git a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
index ff0c907f55..1e449108d8 100644
--- a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
@@ -18,6 +18,7 @@ package com.android.launcher3.shortcuts;
import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@@ -55,7 +56,7 @@ public class DeepShortcutManager {
return false;
}
- public void onShortcutsChanged(List shortcuts) {
+ public void onShortcutsChanged(List shortcuts) {
}
/**
@@ -64,7 +65,7 @@ public class DeepShortcutManager {
* This method is intended to get the full details for shortcuts when they are added or updated,
* because we only get "key" fields in onShortcutsChanged().
*/
- public List queryForFullDetails(String packageName,
+ public List queryForFullDetails(String packageName,
List shortcutIds, UserHandle user) {
return Collections.emptyList();
}
@@ -73,7 +74,7 @@ public class DeepShortcutManager {
* Gets all the manifest and dynamic shortcuts associated with the given package and user,
* to be displayed in the shortcuts container on long press.
*/
- public List queryForShortcutsContainer(ComponentName activity,
+ public List queryForShortcutsContainer(ComponentName activity,
UserHandle user) {
return Collections.emptyList();
}
@@ -96,7 +97,7 @@ public class DeepShortcutManager {
Bundle startActivityOptions, UserHandle user) {
}
- public Drawable getShortcutIconDrawable(ShortcutInfoCompat shortcutInfo, int density) {
+ public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
return null;
}
@@ -105,16 +106,16 @@ public class DeepShortcutManager {
*
* If packageName is null, returns all pinned shortcuts regardless of package.
*/
- public List queryForPinnedShortcuts(String packageName, UserHandle user) {
+ public List queryForPinnedShortcuts(String packageName, UserHandle user) {
return Collections.emptyList();
}
- public List queryForPinnedShortcuts(String packageName,
+ public List queryForPinnedShortcuts(String packageName,
List shortcutIds, UserHandle user) {
return Collections.emptyList();
}
- public List queryForAllShortcuts(UserHandle user) {
+ public List queryForAllShortcuts(UserHandle user) {
return Collections.emptyList();
}
diff --git a/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml b/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml
new file mode 100644
index 0000000000..52cc6fcb62
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
new file mode 100644
index 0000000000..b0f2b4bf8c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml b/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml
new file mode 100644
index 0000000000..b21c34b099
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml b/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml
new file mode 100644
index 0000000000..20c400441a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml
index 1e8d0cc304..7426e30396 100644
--- a/quickstep/recents_ui_overrides/res/values/colors.xml
+++ b/quickstep/recents_ui_overrides/res/values/colors.xml
@@ -1,4 +1,9 @@
#fff
+
+ #61000000
+ #61FFFFFF
+ #3c000000
+ #3cffffff
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
index b654d5c904..f99143581c 100644
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -12,4 +12,13 @@
4dp
10dp
14sp
+
+ 17dp
+ 16dp
+ 8dp
+ 14sp
+ 8dp
+
+ 2dp
+
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
index c60cf5ab69..1ddd3f594f 100644
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ b/quickstep/recents_ui_overrides/res/values/override.xml
@@ -21,6 +21,8 @@
com.android.quickstep.InstantAppResolverImpl
+ com.android.launcher3.appprediction.PredictionAppTracker
+
com.android.quickstep.QuickstepProcessInitializer
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
new file mode 100644
index 0000000000..c5c4add6b2
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 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.launcher3;
+
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+
+import java.util.function.BiPredicate;
+
+public class LauncherInitListenerEx extends LauncherInitListener {
+
+ public LauncherInitListenerEx(BiPredicate onInitListener) {
+ super(onInitListener);
+ }
+
+ @Override
+ protected boolean init(Launcher launcher, boolean alreadyOnHome) {
+ PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW);
+ return super.init(launcher, alreadyOnHome);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
new file mode 100644
index 0000000000..948f39e0fc
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
@@ -0,0 +1,207 @@
+/**
+ * Copyright (C) 2019 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.launcher3.appprediction;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.quickstep.logging.UserEventDispatcherExtension.ALL_APPS_PREDICTION_TIPS;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
+import android.graphics.drawable.ShapeDrawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.graphics.TriangleShape;
+import com.android.systemui.shared.system.LauncherEventUtil;
+
+import androidx.core.content.ContextCompat;
+
+/**
+ * All apps tip view aligned just above prediction apps, shown to users that enter all apps for the
+ * first time.
+ */
+public class AllAppsTipView extends AbstractFloatingView {
+
+ private static final String ALL_APPS_TIP_SEEN = "launcher.all_apps_tip_seen";
+ private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
+ private static final long SHOW_DELAY_MS = 200;
+ private static final long SHOW_DURATION_MS = 300;
+ private static final long HIDE_DURATION_MS = 100;
+
+ private final Launcher mLauncher;
+ private final Handler mHandler = new Handler();
+
+ private AllAppsTipView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ private AllAppsTipView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setOrientation(LinearLayout.VERTICAL);
+
+ mLauncher = Launcher.getLauncher(context);
+
+ init(context);
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ close(true);
+ }
+ return false;
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ if (mIsOpen) {
+ if (animate) {
+ animate().alpha(0f)
+ .withLayer()
+ .setStartDelay(0)
+ .setDuration(HIDE_DURATION_MS)
+ .setInterpolator(Interpolators.ACCEL)
+ .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
+ .start();
+ } else {
+ animate().cancel();
+ mLauncher.getDragLayer().removeView(this);
+ }
+ mLauncher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
+ mIsOpen = false;
+ }
+ }
+
+ @Override
+ public void logActionCommand(int command) {
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_ON_BOARD_POPUP) != 0;
+ }
+
+ private void init(Context context) {
+ inflate(context, R.layout.arrow_toast, this);
+
+ TextView textView = findViewById(R.id.text);
+ textView.setText(R.string.all_apps_prediction_tip);
+
+ View dismissButton = findViewById(R.id.dismiss);
+ dismissButton.setOnClickListener(view -> {
+ mLauncher.getUserEventDispatcher().logActionTip(
+ LauncherEventUtil.DISMISS, ALL_APPS_PREDICTION_TIPS);
+ handleClose(true);
+ });
+
+ View arrowView = findViewById(R.id.arrow);
+ ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
+ ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+ arrowLp.width, arrowLp.height, false));
+ Paint arrowPaint = arrowDrawable.getPaint();
+ TypedValue typedValue = new TypedValue();
+ context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+ arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
+ // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
+ arrowPaint.setPathEffect(new CornerPathEffect(
+ context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
+ arrowView.setBackground(arrowDrawable);
+
+ mIsOpen = true;
+
+ mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
+ }
+
+ private static boolean showAllAppsTipIfNecessary(Launcher launcher) {
+ FloatingHeaderView floatingHeaderView = launcher.getAppsView().getFloatingHeaderView();
+ if (!floatingHeaderView.hasVisibleContent()
+ || AbstractFloatingView.getOpenView(launcher,
+ TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE) != null
+ || !launcher.isInState(ALL_APPS)
+ || hasSeenAllAppsTip(launcher)
+ || UserManagerCompat.getInstance(launcher).isDemoUser()
+ || ActivityManager.isRunningInTestHarness()) {
+ return false;
+ }
+
+ AllAppsTipView allAppsTipView = new AllAppsTipView(launcher.getAppsView().getContext(),
+ null);
+ launcher.getDragLayer().addView(allAppsTipView);
+
+ DragLayer.LayoutParams params = (DragLayer.LayoutParams) allAppsTipView.getLayoutParams();
+ params.gravity = Gravity.CENTER_HORIZONTAL;
+
+ int top = floatingHeaderView.findFixedRowByType(PredictionRowView.class).getTop();
+ allAppsTipView.setY(top - launcher.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_tip_bottom_margin));
+
+ allAppsTipView.setAlpha(0);
+ allAppsTipView.animate()
+ .alpha(1f)
+ .withLayer()
+ .setStartDelay(SHOW_DELAY_MS)
+ .setDuration(SHOW_DURATION_MS)
+ .setInterpolator(Interpolators.DEACCEL)
+ .start();
+
+ launcher.getUserEventDispatcher().logActionTip(
+ LauncherEventUtil.VISIBLE, ALL_APPS_PREDICTION_TIPS);
+ return true;
+ }
+
+ private static boolean hasSeenAllAppsTip(Launcher launcher) {
+ return launcher.getSharedPrefs().getBoolean(ALL_APPS_TIP_SEEN, false);
+ }
+
+ public static void scheduleShowIfNeeded(Launcher launcher) {
+ if (!hasSeenAllAppsTip(launcher)) {
+ launcher.getStateManager().addStateListener(
+ new LauncherStateManager.StateListener() {
+ @Override
+ public void onStateTransitionStart(LauncherState toState) {
+ }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (finalState == ALL_APPS) {
+ if (showAllAppsTipIfNecessary(launcher)) {
+ launcher.getStateManager().removeStateListener(this);
+ }
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
new file mode 100644
index 0000000000..311db21935
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -0,0 +1,308 @@
+/**
+ * Copyright (C) 2019 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.launcher3.appprediction;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.FloatingHeaderRow;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.util.Themes;
+
+import androidx.annotation.ColorInt;
+import androidx.core.content.ContextCompat;
+
+/**
+ * A view which shows a horizontal divider
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class AppsDividerView extends View implements LauncherStateManager.StateListener,
+ FloatingHeaderRow {
+
+ private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
+ private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20;
+
+ public enum DividerType {
+ NONE,
+ LINE,
+ ALL_APPS_LABEL
+ }
+
+ private final Launcher mLauncher;
+ private final TextPaint mPaint = new TextPaint();
+ private DividerType mDividerType = DividerType.NONE;
+
+ private final @ColorInt int mStrokeColor;
+ private final @ColorInt int mAllAppsLabelTextColor;
+
+ private Layout mAllAppsLabelLayout;
+ private boolean mShowAllAppsLabel;
+
+ private FloatingHeaderView mParent;
+ private boolean mTabsHidden;
+ private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS;
+
+ private boolean mIsScrolledOut = false;
+
+ public AppsDividerView(Context context) {
+ this(context, null);
+ }
+
+ public AppsDividerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(context);
+
+ boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark);
+ mPaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
+
+ mStrokeColor = ContextCompat.getColor(context, isMainColorDark
+ ? R.color.all_apps_prediction_row_separator_dark
+ : R.color.all_apps_prediction_row_separator);
+
+ mAllAppsLabelTextColor = ContextCompat.getColor(context, isMainColorDark
+ ? R.color.all_apps_label_text_dark
+ : R.color.all_apps_label_text);
+ }
+
+ public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
+ mParent = parent;
+ mTabsHidden = tabsHidden;
+ mRows = rows;
+ updateDividerType();
+ }
+
+ @Override
+ public int getExpectedHeight() {
+ return getPaddingTop() + getPaddingBottom();
+ }
+
+ @Override
+ public boolean shouldDraw() {
+ return mDividerType != DividerType.NONE;
+ }
+
+ @Override
+ public boolean hasVisibleContent() {
+ return false;
+ }
+
+ private void updateDividerType() {
+ final DividerType dividerType;
+ if (!mTabsHidden) {
+ dividerType = DividerType.NONE;
+ } else {
+ // Check how many sections above me.
+ int sectionCount = 0;
+ for (FloatingHeaderRow row : mRows) {
+ if (row == this) {
+ break;
+ } else if (row.shouldDraw()) {
+ sectionCount ++;
+ }
+ }
+
+ if (mShowAllAppsLabel && sectionCount > 0) {
+ dividerType = DividerType.ALL_APPS_LABEL;
+ } else if (sectionCount == 1) {
+ dividerType = DividerType.LINE;
+ } else {
+ dividerType = DividerType.NONE;
+ }
+ }
+
+ if (mDividerType != dividerType) {
+ mDividerType = dividerType;
+ int topPadding;
+ int bottomPadding;
+ switch (dividerType) {
+ case LINE:
+ topPadding = 0;
+ bottomPadding = getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
+ mPaint.setColor(mStrokeColor);
+ break;
+ case ALL_APPS_LABEL:
+ topPadding = getAllAppsLabelLayout().getHeight() + getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_label_top_padding);
+ bottomPadding = getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding);
+ mPaint.setColor(mAllAppsLabelTextColor);
+ break;
+ case NONE:
+ default:
+ topPadding = bottomPadding = 0;
+ break;
+ }
+ setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding);
+ updateViewVisibility();
+ invalidate();
+ requestLayout();
+ if (mParent != null) {
+ mParent.onHeightUpdated();
+ }
+ }
+ }
+
+ private void updateViewVisibility() {
+ setVisibility(mDividerType == DividerType.NONE
+ ? GONE
+ : (mIsScrolledOut ? INVISIBLE : VISIBLE));
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mDividerType == DividerType.LINE) {
+ int side = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
+ int y = getHeight() - (getPaddingBottom() / 2);
+ int x1 = getPaddingLeft() + side;
+ int x2 = getWidth() - getPaddingRight() - side;
+ canvas.drawLine(x1, y, x2, y, mPaint);
+ } else if (mDividerType == DividerType.ALL_APPS_LABEL) {
+ Layout textLayout = getAllAppsLabelLayout();
+ int x = getWidth() / 2 - textLayout.getWidth() / 2;
+ int y = getHeight() - getPaddingBottom() - textLayout.getHeight();
+ canvas.translate(x, y);
+ textLayout.draw(canvas);
+ canvas.translate(-x, -y);
+ }
+ }
+
+ private Layout getAllAppsLabelLayout() {
+ if (mAllAppsLabelLayout == null) {
+ mPaint.setAntiAlias(true);
+ mPaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
+ mPaint.setTextSize(
+ getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size));
+
+ CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label);
+ mAllAppsLabelLayout = StaticLayout.Builder.obtain(
+ allAppsLabelText, 0, allAppsLabelText.length(), mPaint,
+ Math.round(mPaint.measureText(allAppsLabelText.toString())))
+ .setAlignment(Layout.Alignment.ALIGN_CENTER)
+ .setMaxLines(1)
+ .setIncludePad(true)
+ .build();
+ }
+ return mAllAppsLabelLayout;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
+ getPaddingBottom() + getPaddingTop());
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (shouldShowAllAppsLabel()) {
+ mShowAllAppsLabel = true;
+ mLauncher.getStateManager().addStateListener(this);
+ updateDividerType();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mLauncher.getStateManager().removeStateListener(this);
+ }
+
+ @Override
+ public void onStateTransitionStart(LauncherState toState) { }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (finalState == ALL_APPS) {
+ setAllAppsVisitedCount(getAllAppsVisitedCount() + 1);
+ } else {
+ if (mShowAllAppsLabel != shouldShowAllAppsLabel()) {
+ mShowAllAppsLabel = !mShowAllAppsLabel;
+ updateDividerType();
+ }
+
+ if (!mShowAllAppsLabel) {
+ mLauncher.getStateManager().removeStateListener(this);
+ }
+ }
+ }
+
+ private void setAllAppsVisitedCount(int count) {
+ mLauncher.getSharedPrefs().edit().putInt(ALL_APPS_VISITED_COUNT, count).apply();
+ }
+
+ private int getAllAppsVisitedCount() {
+ return mLauncher.getSharedPrefs().getInt(ALL_APPS_VISITED_COUNT, 0);
+ }
+
+ private boolean shouldShowAllAppsLabel() {
+ return getAllAppsVisitedCount() < SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT;
+ }
+
+ @Override
+ public void setInsets(Rect insets, DeviceProfile grid) {
+ int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
+ + grid.cellLayoutPaddingLeftRightPx;
+ setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
+ }
+
+ @Override
+ public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
+ PropertySetter setter, Interpolator fadeInterpolator) {
+ // Don't use setViewAlpha as we want to control the visibility ourselves.
+ setter.setFloat(this, ALPHA, hasContent ? 1 : 0, fadeInterpolator);
+ }
+
+ @Override
+ public void setVerticalScroll(int scroll, boolean isScrolledOut) {
+ setTranslationY(scroll);
+ mIsScrolledOut = isScrolledOut;
+ updateViewVisibility();
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return AppsDividerView.class;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
new file mode 100644
index 0000000000..b9f4147f85
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (C) 2019 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.launcher3.appprediction;
+
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.content.Context;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ComponentKey;
+
+public class ComponentKeyMapper {
+
+ protected final ComponentKey componentKey;
+ private final Context mContext;
+ private final DynamicItemCache mCache;
+
+ public ComponentKeyMapper(Context context, ComponentKey key, DynamicItemCache cache) {
+ mContext = context;
+ componentKey = key;
+ mCache = cache;
+ }
+
+ public String getPackage() {
+ return componentKey.componentName.getPackageName();
+ }
+
+ public String getComponentClass() {
+ return componentKey.componentName.getClassName();
+ }
+
+ public ComponentKey getComponentKey() {
+ return componentKey;
+ }
+
+ @Override
+ public String toString() {
+ return componentKey.toString();
+ }
+
+ public ItemInfoWithIcon getApp(AllAppsStore store) {
+ AppInfo item = store.getApp(componentKey);
+ if (item != null) {
+ return item;
+ } else if (getComponentClass().equals(COMPONENT_CLASS_MARKER)) {
+ return mCache.getInstantApp(componentKey.componentName.getPackageName());
+ } else if (componentKey instanceof ShortcutKey) {
+ return mCache.getShortcutInfo((ShortcutKey) componentKey);
+ }
+ return null;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
new file mode 100644
index 0000000000..4ecc39cf6a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (C) 2019 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.launcher3.appprediction;
+
+import static android.content.pm.PackageManager.MATCH_INSTANT;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.InstantAppResolver;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+/**
+ * Utility class which loads and caches predicted items like instant apps and shortcuts, before
+ * they can be displayed on the UI
+ */
+public class DynamicItemCache {
+
+ private static final String TAG = "DynamicItemCache";
+ private static final boolean DEBUG = false;
+ private static final String DEFAULT_URL = "default-url";
+
+ private static final int BG_MSG_LOAD_SHORTCUTS = 1;
+ private static final int BG_MSG_LOAD_INSTANT_APPS = 2;
+
+ private static final int UI_MSG_UPDATE_SHORTCUTS = 1;
+ private static final int UI_MSG_UPDATE_INSTANT_APPS = 2;
+
+ private final Context mContext;
+ private final Handler mWorker;
+ private final Handler mUiHandler;
+ private final InstantAppResolver mInstantAppResolver;
+ private final Runnable mOnUpdateCallback;
+
+ private final Map mShortcuts;
+ private final Map mInstantApps;
+
+ public DynamicItemCache(Context context, Runnable onUpdateCallback) {
+ mContext = context;
+ mWorker = new Handler(LauncherModel.getWorkerLooper(), this::handleWorkerMessage);
+ mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
+ mInstantAppResolver = InstantAppResolver.newInstance(context);
+ mOnUpdateCallback = onUpdateCallback;
+
+ mShortcuts = new HashMap<>();
+ mInstantApps = new HashMap<>();
+ }
+
+ public void cacheItems(List shortcutKeys, List pkgNames) {
+ if (!shortcutKeys.isEmpty()) {
+ mWorker.removeMessages(BG_MSG_LOAD_SHORTCUTS);
+ Message.obtain(mWorker, BG_MSG_LOAD_SHORTCUTS, shortcutKeys).sendToTarget();
+ }
+ if (!pkgNames.isEmpty()) {
+ mWorker.removeMessages(BG_MSG_LOAD_INSTANT_APPS);
+ Message.obtain(mWorker, BG_MSG_LOAD_INSTANT_APPS, pkgNames).sendToTarget();
+ }
+ }
+
+ private boolean handleWorkerMessage(Message msg) {
+ switch (msg.what) {
+ case BG_MSG_LOAD_SHORTCUTS: {
+ List shortcutKeys = msg.obj != null ?
+ (List) msg.obj : Collections.EMPTY_LIST;
+ Map shortcutKeyAndInfos = new ArrayMap<>();
+ for (ShortcutKey shortcutKey : shortcutKeys) {
+ WorkspaceItemInfo workspaceItemInfo = loadShortcutWorker(shortcutKey);
+ if (workspaceItemInfo != null) {
+ shortcutKeyAndInfos.put(shortcutKey, workspaceItemInfo);
+ }
+ }
+ Message.obtain(mUiHandler, UI_MSG_UPDATE_SHORTCUTS, shortcutKeyAndInfos)
+ .sendToTarget();
+ return true;
+ }
+ case BG_MSG_LOAD_INSTANT_APPS: {
+ List pkgNames = msg.obj != null ?
+ (List) msg.obj : Collections.EMPTY_LIST;
+ List instantAppItemInfos = new ArrayList<>();
+ for (String pkgName : pkgNames) {
+ InstantAppItemInfo instantAppItemInfo = loadInstantApp(pkgName);
+ if (instantAppItemInfo != null) {
+ instantAppItemInfos.add(instantAppItemInfo);
+ }
+ }
+ Message.obtain(mUiHandler, UI_MSG_UPDATE_INSTANT_APPS, instantAppItemInfos)
+ .sendToTarget();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean handleUiMessage(Message msg) {
+ switch (msg.what) {
+ case UI_MSG_UPDATE_SHORTCUTS: {
+ mShortcuts.clear();
+ mShortcuts.putAll((Map) msg.obj);
+ mOnUpdateCallback.run();
+ return true;
+ }
+ case UI_MSG_UPDATE_INSTANT_APPS: {
+ List instantAppItemInfos = (List) msg.obj;
+ mInstantApps.clear();
+ for (InstantAppItemInfo instantAppItemInfo : instantAppItemInfos) {
+ mInstantApps.put(instantAppItemInfo.getTargetComponent().getPackageName(),
+ instantAppItemInfo);
+ }
+ mOnUpdateCallback.run();
+ if (DEBUG) {
+ Log.d(TAG, String.format("Cache size: %d, Cache: %s",
+ mInstantApps.size(), mInstantApps.toString()));
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @WorkerThread
+ private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) {
+ DeepShortcutManager mgr = DeepShortcutManager.getInstance(mContext);
+ List details = mgr.queryForFullDetails(
+ shortcutKey.componentName.getPackageName(),
+ Collections.singletonList(shortcutKey.getId()),
+ shortcutKey.user);
+ if (!details.isEmpty()) {
+ WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
+ try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
+ si.applyFrom(li.createShortcutIcon(details.get(0), true /* badged */, null));
+ } catch (Exception e) {
+ if (DEBUG) {
+ Log.e(TAG, "Error loading shortcut icon for " + shortcutKey.toString());
+ }
+ return null;
+ }
+ return si;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "No shortcut found: " + shortcutKey.toString());
+ }
+ return null;
+ }
+
+ private InstantAppItemInfo loadInstantApp(String pkgName) {
+ PackageManager pm = mContext.getPackageManager();
+
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0);
+ if (!mInstantAppResolver.isInstantApp(ai)) {
+ return null;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+
+ String url = retrieveDefaultUrl(pkgName, pm);
+ if (url == null) {
+ Log.w(TAG, "no default-url available for pkg " + pkgName);
+ return null;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .addCategory(Intent.CATEGORY_BROWSABLE)
+ .setData(Uri.parse(url));
+ InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName);
+ IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+ iconCache.getTitleAndIcon(info, false);
+ if (info.iconBitmap == null || iconCache.isDefaultIcon(info.iconBitmap, info.user)) {
+ return null;
+ }
+ return info;
+ }
+
+ @Nullable
+ public static String retrieveDefaultUrl(String pkgName, PackageManager pm) {
+ Intent mainIntent = new Intent().setAction(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_LAUNCHER).setPackage(pkgName);
+ List resolveInfos = pm.queryIntentActivities(
+ mainIntent, MATCH_INSTANT | PackageManager.GET_META_DATA);
+ String url = null;
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ if (resolveInfo.activityInfo.metaData != null
+ && resolveInfo.activityInfo.metaData.containsKey(DEFAULT_URL)) {
+ url = resolveInfo.activityInfo.metaData.getString(DEFAULT_URL);
+ }
+ }
+ return url;
+ }
+
+ @UiThread
+ public InstantAppItemInfo getInstantApp(String pkgName) {
+ return mInstantApps.get(pkgName);
+ }
+
+ @MainThread
+ public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
+ return mShortcuts.get(key);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
new file mode 100644
index 0000000000..6e5f4617f4
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (C) 2019 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.launcher3.appprediction;
+
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.content.ComponentName;
+import android.content.Intent;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.WorkspaceItemInfo;
+
+public class InstantAppItemInfo extends AppInfo {
+
+ public InstantAppItemInfo(Intent intent, String packageName) {
+ this.intent = intent;
+ this.componentName = new ComponentName(packageName, COMPONENT_CLASS_MARKER);
+ }
+
+ @Override
+ public ComponentName getTargetComponent() {
+ return componentName;
+ }
+
+ @Override
+ public WorkspaceItemInfo makeWorkspaceItem() {
+ WorkspaceItemInfo workspaceItemInfo = super.makeWorkspaceItem();
+ workspaceItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ workspaceItemInfo.status = WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON
+ | WorkspaceItemInfo.FLAG_RESTORE_STARTED
+ | WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI;
+ workspaceItemInfo.intent.setPackage(componentName.getPackageName());
+ return workspaceItemInfo;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
new file mode 100644
index 0000000000..bd78573307
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
@@ -0,0 +1,214 @@
+/**
+ * Copyright (C) 2019 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.launcher3.appprediction;
+
+import static com.android.launcher3.appprediction.PredictionUiStateManager.KEY_APP_SUGGESTION;
+
+import android.annotation.TargetApi;
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.util.UiThreadHelper;
+
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+/**
+ * Subclass of app tracker which publishes the data to the prediction engine and gets back results.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public class PredictionAppTracker extends AppLaunchTracker
+ implements OnSharedPreferenceChangeListener {
+
+ private static final String TAG = "PredictionAppTracker";
+ private static final boolean DBG = false;
+
+ private static final int MSG_INIT = 0;
+ private static final int MSG_DESTROY = 1;
+ private static final int MSG_LAUNCH = 2;
+ private static final int MSG_PREDICT = 3;
+
+ private final Context mContext;
+ private final Handler mMessageHandler;
+
+ private boolean mEnabled;
+
+ // Accessed only on worker thread
+ private AppPredictor mHomeAppPredictor;
+ private AppPredictor mRecentsOverviewPredictor;
+
+ public PredictionAppTracker(Context context) {
+ mContext = context;
+ mMessageHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleMessage);
+
+ SharedPreferences prefs = Utilities.getPrefs(context);
+ setEnabled(prefs.getBoolean(KEY_APP_SUGGESTION, true));
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged);
+ }
+
+ @UiThread
+ private void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+ // Reinitialize everything
+ setEnabled(mEnabled);
+ }
+
+ @Override
+ @UiThread
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ if (KEY_APP_SUGGESTION.equals(key)) {
+ setEnabled(prefs.getBoolean(KEY_APP_SUGGESTION, true));
+ }
+ }
+
+ @WorkerThread
+ private void destroy() {
+ if (mHomeAppPredictor != null) {
+ mHomeAppPredictor.destroy();
+ mHomeAppPredictor = null;
+ }
+ if (mRecentsOverviewPredictor != null) {
+ mRecentsOverviewPredictor.destroy();
+ mRecentsOverviewPredictor = null;
+ }
+ }
+
+ @WorkerThread
+ private AppPredictor createPredictor(Client client, int count) {
+ AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
+
+ AppPredictor predictor = apm.createAppPredictionSession(
+ new AppPredictionContext.Builder(mContext)
+ .setUiSurface(client.id)
+ .setPredictedTargetCount(count)
+ .build());
+ predictor.registerPredictionUpdates(mContext.getMainExecutor(),
+ PredictionUiStateManager.INSTANCE.get(mContext).appPredictorCallback(client));
+ predictor.requestPredictionUpdate();
+ return predictor;
+ }
+
+ @WorkerThread
+ private boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_INIT: {
+ // Destroy any existing clients
+ destroy();
+
+ // Initialize the clients
+ int count = InvariantDeviceProfile.INSTANCE.get(mContext).numColumns;
+ mHomeAppPredictor = createPredictor(Client.HOME, count);
+ mRecentsOverviewPredictor = createPredictor(Client.OVERVIEW, count);
+ return true;
+ }
+ case MSG_DESTROY: {
+ destroy();
+ return true;
+ }
+ case MSG_LAUNCH: {
+ if (mEnabled && mHomeAppPredictor != null) {
+ mHomeAppPredictor.notifyAppTargetEvent((AppTargetEvent) msg.obj);
+ }
+ return true;
+ }
+ case MSG_PREDICT: {
+ if (mEnabled && mHomeAppPredictor != null) {
+ String client = (String) msg.obj;
+ if (Client.HOME.id.equals(client)) {
+ mHomeAppPredictor.requestPredictionUpdate();
+ } else {
+ mRecentsOverviewPredictor.requestPredictionUpdate();
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ @UiThread
+ public void onReturnedToHome() {
+ String client = Client.HOME.id;
+ mMessageHandler.removeMessages(MSG_PREDICT, client);
+ Message.obtain(mMessageHandler, MSG_PREDICT, client).sendToTarget();
+ if (DBG) {
+ Log.d(TAG, String.format("Sent immediate message to update %s", client));
+ }
+ }
+
+ @UiThread
+ public void setEnabled(boolean isEnabled) {
+ mEnabled = isEnabled;
+ if (isEnabled) {
+ mMessageHandler.removeMessages(MSG_DESTROY);
+ mMessageHandler.sendEmptyMessage(MSG_INIT);
+ } else {
+ mMessageHandler.removeMessages(MSG_INIT);
+ mMessageHandler.sendEmptyMessage(MSG_DESTROY);
+ }
+ }
+
+ @Override
+ @UiThread
+ public void onStartShortcut(String packageName, String shortcutId, UserHandle user,
+ String container) {
+ // TODO: Use the full shortcut info
+ AppTarget target = new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutId))
+ .setTarget(packageName, user)
+ .setClassName(shortcutId)
+ .build();
+ sendLaunch(target, container);
+ }
+
+ @Override
+ @UiThread
+ public void onStartApp(ComponentName cn, UserHandle user, String container) {
+ if (cn != null) {
+ AppTarget target = new AppTarget.Builder(new AppTargetId("app:" + cn))
+ .setTarget(cn.getPackageName(), user)
+ .setClassName(cn.getClassName())
+ .build();
+ sendLaunch(target, container);
+ }
+ }
+
+ @UiThread
+ private void sendLaunch(AppTarget target, String container) {
+ AppTargetEvent event = new AppTargetEvent.Builder(target, AppTargetEvent.ACTION_LAUNCH)
+ .setLaunchLocation(container == null ? CONTAINER_DEFAULT : container)
+ .build();
+ Message.obtain(mMessageHandler, MSG_LAUNCH, event).sendToTarget();
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
new file mode 100644
index 0000000000..55f4c98e9a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -0,0 +1,411 @@
+/**
+ * Copyright (C) 2019 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.launcher3.appprediction;
+
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.IntProperty;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.allapps.FloatingHeaderRow;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.keyboard.FocusIndicatorHelper;
+import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
+import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.util.Themes;
+import com.android.quickstep.AnimatedFloat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@TargetApi(Build.VERSION_CODES.P)
+public class PredictionRowView extends LinearLayout implements
+ LogContainerProvider, OnDeviceProfileChangeListener, FloatingHeaderRow {
+
+ private static final String TAG = "PredictionRowView";
+
+ private static final IntProperty TEXT_ALPHA =
+ new IntProperty("textAlpha") {
+ @Override
+ public void setValue(PredictionRowView view, int alpha) {
+ view.setTextAlpha(alpha);
+ }
+
+ @Override
+ public Integer get(PredictionRowView view) {
+ return view.mIconCurrentTextAlpha;
+ }
+ };
+
+ private static final Interpolator ALPHA_FACTOR_INTERPOLATOR =
+ (t) -> (t < 0.8f) ? 0 : (t - 0.8f) / 0.2f;
+
+ private static final OnClickListener PREDICTION_CLICK_LISTENER =
+ ItemClickHandler.getInstance(AppLaunchTracker.CONTAINER_PREDICTIONS);
+
+ private final Launcher mLauncher;
+ private final PredictionUiStateManager mPredictionUiStateManager;
+ private final int mNumPredictedAppsPerRow;
+
+ // The set of predicted app component names
+ private final List mPredictedAppComponents = new ArrayList<>();
+ // The set of predicted apps resolved from the component names and the current set of apps
+ private final ArrayList mPredictedApps = new ArrayList<>();
+ // Helper to drawing the focus indicator.
+ private final FocusIndicatorHelper mFocusHelper;
+
+ private final int mIconTextColor;
+ private final int mIconFullTextAlpha;
+ private int mIconCurrentTextAlpha;
+
+ private FloatingHeaderView mParent;
+ private boolean mScrolledOut;
+
+ private float mScrollTranslation = 0;
+ private final AnimatedFloat mContentAlphaFactor =
+ new AnimatedFloat(this::updateTranslationAndAlpha);
+ private final AnimatedFloat mOverviewScrollFactor =
+ new AnimatedFloat(this::updateTranslationAndAlpha);
+
+ private View mLoadingProgress;
+
+ private boolean mPredictionsEnabled = false;
+
+ public PredictionRowView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ setOrientation(LinearLayout.HORIZONTAL);
+
+ mFocusHelper = new SimpleFocusIndicatorHelper(this);
+
+ mNumPredictedAppsPerRow = LauncherAppState.getIDP(context).numColumns;
+ mLauncher = Launcher.getLauncher(context);
+ mLauncher.addOnDeviceProfileChangeListener(this);
+
+ mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(context);
+
+ mIconTextColor = Themes.getAttrColor(context, android.R.attr.textColorSecondary);
+ mIconFullTextAlpha = Color.alpha(mIconTextColor);
+ mIconCurrentTextAlpha = mIconFullTextAlpha;
+
+ updateVisibility();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mPredictionUiStateManager.setTargetAppsView(mLauncher.getAppsView());
+ getAppsStore().registerIconContainer(this);
+ AllAppsTipView.scheduleShowIfNeeded(mLauncher);
+ }
+
+ private AllAppsStore getAppsStore() {
+ return mLauncher.getAppsView().getAppsStore();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ mPredictionUiStateManager.setTargetAppsView(null);
+ getAppsStore().unregisterIconContainer(this);
+ }
+
+ public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
+ mParent = parent;
+ setPredictionsEnabled(mPredictionUiStateManager.arePredictionsEnabled());
+ }
+
+ private void setPredictionsEnabled(boolean predictionsEnabled) {
+ if (predictionsEnabled != mPredictionsEnabled) {
+ mPredictionsEnabled = predictionsEnabled;
+ updateVisibility();
+ }
+ }
+
+ private void updateVisibility() {
+ setVisibility(mPredictionsEnabled ? VISIBLE : GONE);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(),
+ MeasureSpec.EXACTLY));
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ mFocusHelper.draw(canvas);
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
+ public int getExpectedHeight() {
+ return getVisibility() == GONE ? 0 :
+ Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx
+ + getPaddingTop() + getPaddingBottom();
+ }
+
+ @Override
+ public boolean shouldDraw() {
+ return getVisibility() != GONE;
+ }
+
+ @Override
+ public boolean hasVisibleContent() {
+ return mPredictionUiStateManager.arePredictionsEnabled();
+ }
+
+ /**
+ * Returns the predicted apps.
+ */
+ public List getPredictedApps() {
+ return mPredictedApps;
+ }
+
+ /**
+ * Sets the current set of predicted apps.
+ *
+ * This can be called before we get the full set of applications, we should merge the results
+ * only in onPredictionsUpdated() which is idempotent.
+ *
+ * If the number of predicted apps is the same as the previous list of predicted apps,
+ * we can optimize by swapping them in place.
+ */
+ public void setPredictedApps(boolean predictionsEnabled, List apps) {
+ setPredictionsEnabled(predictionsEnabled);
+ mPredictedAppComponents.clear();
+ mPredictedAppComponents.addAll(apps);
+
+ mPredictedApps.clear();
+ mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
+ applyPredictionApps();
+ }
+
+ @Override
+ public void onDeviceProfileChanged(DeviceProfile dp) {
+ removeAllViews();
+ applyPredictionApps();
+ }
+
+ private void applyPredictionApps() {
+ if (mLoadingProgress != null) {
+ removeView(mLoadingProgress);
+ }
+ if (!mPredictionsEnabled) {
+ mParent.onHeightUpdated();
+ return;
+ }
+
+ if (getChildCount() != mNumPredictedAppsPerRow) {
+ while (getChildCount() > mNumPredictedAppsPerRow) {
+ removeViewAt(0);
+ }
+ while (getChildCount() < mNumPredictedAppsPerRow) {
+ BubbleTextView icon = (BubbleTextView) mLauncher.getLayoutInflater().inflate(
+ R.layout.all_apps_icon, this, false);
+ icon.setOnClickListener(PREDICTION_CLICK_LISTENER);
+ icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS);
+ icon.setLongPressTimeoutFactor(1f);
+ icon.setOnFocusChangeListener(mFocusHelper);
+
+ LayoutParams lp = (LayoutParams) icon.getLayoutParams();
+ // Ensure the all apps icon height matches the workspace icons in portrait mode.
+ lp.height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
+ lp.width = 0;
+ lp.weight = 1;
+ addView(icon);
+ }
+ }
+
+ int predictionCount = mPredictedApps.size();
+ int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha);
+
+ for (int i = 0; i < getChildCount(); i++) {
+ BubbleTextView icon = (BubbleTextView) getChildAt(i);
+ icon.reset();
+ if (predictionCount > i) {
+ icon.setVisibility(View.VISIBLE);
+ if (mPredictedApps.get(i) instanceof AppInfo) {
+ icon.applyFromApplicationInfo((AppInfo) mPredictedApps.get(i));
+ } else if (mPredictedApps.get(i) instanceof WorkspaceItemInfo) {
+ icon.applyFromWorkspaceItem((WorkspaceItemInfo) mPredictedApps.get(i));
+ }
+ icon.setTextColor(iconColor);
+ } else {
+ icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE);
+ }
+ }
+
+ if (predictionCount == 0) {
+ if (mLoadingProgress == null) {
+ mLoadingProgress = LayoutInflater.from(getContext())
+ .inflate(R.layout.prediction_load_progress, this, false);
+ }
+ addView(mLoadingProgress);
+ } else {
+ mLoadingProgress = null;
+ }
+
+ mParent.onHeightUpdated();
+ }
+
+ private List processPredictedAppComponents(List components) {
+ if (getAppsStore().getApps().isEmpty()) {
+ // Apps have not been bound yet.
+ return Collections.emptyList();
+ }
+
+ List predictedApps = new ArrayList<>();
+ for (ComponentKeyMapper mapper : components) {
+ ItemInfoWithIcon info = mapper.getApp(getAppsStore());
+ if (info != null) {
+ predictedApps.add(info);
+ } else {
+ if (FeatureFlags.IS_DOGFOOD_BUILD) {
+ Log.e(TAG, "Predicted app not found: " + mapper);
+ }
+ }
+ // Stop at the number of predicted apps
+ if (predictedApps.size() == mNumPredictedAppsPerRow) {
+ break;
+ }
+ }
+ return predictedApps;
+ }
+
+ @Override
+ public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+ LauncherLogProto.Target targetParent) {
+ for (int i = 0; i < mPredictedApps.size(); i++) {
+ ItemInfoWithIcon appInfo = mPredictedApps.get(i);
+ if (appInfo == info) {
+ targetParent.containerType = LauncherLogProto.ContainerType.PREDICTION;
+ target.predictedRank = i;
+ break;
+ }
+ }
+ }
+
+ public void setTextAlpha(int alpha) {
+ mIconCurrentTextAlpha = alpha;
+ int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha);
+
+ if (mLoadingProgress == null) {
+ for (int i = 0; i < getChildCount(); i++) {
+ ((BubbleTextView) getChildAt(i)).setTextColor(iconColor);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+
+ @Override
+ public void setVerticalScroll(int scroll, boolean isScrolledOut) {
+ mScrolledOut = isScrolledOut;
+ updateTranslationAndAlpha();
+ if (!isScrolledOut) {
+ mScrollTranslation = scroll;
+ updateTranslationAndAlpha();
+ }
+ }
+
+ private void updateTranslationAndAlpha() {
+ if (mPredictionsEnabled) {
+ setTranslationY((1 - mOverviewScrollFactor.value) * mScrollTranslation);
+
+ float factor = ALPHA_FACTOR_INTERPOLATOR.getInterpolation(mOverviewScrollFactor.value);
+ float endAlpha = factor + (1 - factor) * (mScrolledOut ? 0 : 1);
+ setAlpha(mContentAlphaFactor.value * endAlpha);
+ AlphaUpdateListener.updateVisibility(this);
+ }
+ }
+
+ @Override
+ public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
+ PropertySetter setter, Interpolator fadeInterpolator) {
+ boolean isDrawn = getAlpha() > 0;
+ int textAlpha = hasHeaderExtra
+ ? (hasContent ? mIconFullTextAlpha : 0) // Text follows the content visibility
+ : mIconCurrentTextAlpha; // Leave as before
+ if (!isDrawn) {
+ // If the header is not drawn, no need to animate the text alpha
+ setTextAlpha(textAlpha);
+ } else {
+ setter.setInt(this, TEXT_ALPHA, textAlpha, fadeInterpolator);
+ }
+
+ setter.setFloat(mOverviewScrollFactor, AnimatedFloat.VALUE,
+ (hasHeaderExtra && !hasContent) ? 1 : 0, LINEAR);
+ setter.setFloat(mContentAlphaFactor, AnimatedFloat.VALUE, hasHeaderExtra ? 1 : 0,
+ fadeInterpolator);
+ }
+
+ @Override
+ public void setInsets(Rect insets, DeviceProfile grid) {
+ int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
+ + grid.cellLayoutPaddingLeftRightPx;
+ setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return PredictionRowView.class;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
new file mode 100644
index 0000000000..54fd84522d
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -0,0 +1,330 @@
+/**
+ * Copyright (C) 2019 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.launcher3.appprediction;
+
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.MainThreadInitializedObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Handler responsible to updating the UI due to predicted apps changes. Operations:
+ * 1) Pushes the predicted apps to all-apps. If all-apps is visible, waits until it becomes
+ * invisible again before applying the changes. This ensures that the UI does not change abruptly
+ * in front of the user, even if an app launched and user pressed back button to return to the
+ * all-apps UI again.
+ * 2) Prefetch high-res icons for predicted apps. This ensures that we have the icons in memory
+ * even if all-apps is not opened as they are shown in search UI as well
+ * 3) Load instant app if it is not already in memory. As predictions are persisted on disk,
+ * instant app will not be in memory when launcher starts.
+ * 4) Maintains the current active client id (for the predictions) and all updates are performed on
+ * that client id.
+ */
+public class PredictionUiStateManager implements OnGlobalLayoutListener, ItemInfoUpdateReceiver,
+ OnSharedPreferenceChangeListener, OnIDPChangeListener, OnUpdateListener {
+
+ public static final String KEY_APP_SUGGESTION = "pref_show_predictions";
+
+ // TODO (b/129421797): Update the client constants
+ public enum Client {
+ HOME("GEL"),
+ OVERVIEW("OVERVIEW_GEL");
+
+ public final String id;
+
+ Client(String id) {
+ this.id = id;
+ }
+ }
+
+ public static final MainThreadInitializedObject INSTANCE =
+ new MainThreadInitializedObject<>(PredictionUiStateManager::new);
+
+ private final Context mContext;
+ private final SharedPreferences mMainPrefs;
+
+ private final DynamicItemCache mDynamicItemCache;
+ private final List[] mPredictionServicePredictions;
+
+ private int mMaxIconsPerRow;
+ private Client mActiveClient;
+
+ private AllAppsContainerView mAppsView;
+
+ private PredictionState mPendingState;
+ private PredictionState mCurrentState;
+
+ private PredictionUiStateManager(Context context) {
+ mContext = context;
+ mMainPrefs = Utilities.getPrefs(context);
+
+ mDynamicItemCache = new DynamicItemCache(context, this::onAppsUpdated);
+
+ mActiveClient = Client.HOME;
+
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+ mMaxIconsPerRow = idp.numColumns;
+
+ idp.addOnChangeListener(this);
+ mPredictionServicePredictions = new List[Client.values().length];
+ for (int i = 0; i < mPredictionServicePredictions.length; i++) {
+ mPredictionServicePredictions[i] = Collections.emptyList();
+ }
+ // Listens for enable/disable signal, and predictions if using AiAi is disabled.
+ mMainPrefs.registerOnSharedPreferenceChangeListener(this);
+ // Call this last
+ mCurrentState = parseLastState();
+ }
+
+ @Override
+ public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+ mMaxIconsPerRow = profile.numColumns;
+ }
+
+ public Client getClient() {
+ return mActiveClient;
+ }
+
+ public void switchClient(Client client) {
+ if (client == mActiveClient) {
+ return;
+ }
+ mActiveClient = client;
+ dispatchOnChange(true);
+ }
+
+ public void setTargetAppsView(AllAppsContainerView appsView) {
+ if (mAppsView != null) {
+ mAppsView.getAppsStore().removeUpdateListener(this);
+ }
+ mAppsView = appsView;
+ if (mAppsView != null) {
+ mAppsView.getAppsStore().addUpdateListener(this);
+ }
+ if (mPendingState != null) {
+ applyState(mPendingState);
+ mPendingState = null;
+ } else {
+ applyState(mCurrentState);
+ }
+ updateDependencies(mCurrentState);
+ }
+
+ @Override
+ public void reapplyItemInfo(ItemInfoWithIcon info) { }
+
+ @Override
+ public void onGlobalLayout() {
+ if (mAppsView == null) {
+ return;
+ }
+ if (mPendingState != null && canApplyPredictions(mPendingState)) {
+ applyState(mPendingState);
+ mPendingState = null;
+ }
+ if (mPendingState == null) {
+ mAppsView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ }
+
+ private void scheduleApplyPredictedApps(PredictionState state) {
+ boolean registerListener = mPendingState == null;
+ mPendingState = state;
+ if (registerListener) {
+ // OnGlobalLayoutListener is called whenever a view in the view tree changes
+ // visibility. Add a listener and wait until appsView is invisible again.
+ mAppsView.getViewTreeObserver().addOnGlobalLayoutListener(this);
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ if (KEY_APP_SUGGESTION.equals(key)) {
+ dispatchOnChange(true);
+ }
+ }
+
+ private void applyState(PredictionState state) {
+ boolean wasEnabled = mCurrentState.isEnabled;
+ mCurrentState = state;
+ if (mAppsView != null) {
+ mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
+ .setPredictedApps(mCurrentState.isEnabled, mCurrentState.apps);
+
+ if (wasEnabled != mCurrentState.isEnabled) {
+ // Reapply state as the State UI might have changed.
+ Launcher.getLauncher(mAppsView.getContext()).getStateManager().reapplyState(true);
+ }
+ }
+ }
+
+ public AppPredictor.Callback appPredictorCallback(Client client) {
+ return targets -> {
+ mPredictionServicePredictions[client.ordinal()] = targets;
+ dispatchOnChange(true);
+ };
+ }
+
+ private void dispatchOnChange(boolean changed) {
+ PredictionState newState = changed ? parseLastState() :
+ (mPendingState == null ? mCurrentState : mPendingState);
+ if (changed && mAppsView != null && !canApplyPredictions(newState)) {
+ scheduleApplyPredictedApps(newState);
+ } else {
+ applyState(newState);
+ }
+ }
+
+ private PredictionState parseLastState() {
+ PredictionState state = new PredictionState();
+ state.isEnabled = mMainPrefs.getBoolean(KEY_APP_SUGGESTION, true);
+ if (!state.isEnabled) {
+ state.apps = Collections.EMPTY_LIST;
+ return state;
+ }
+
+ state.apps = new ArrayList<>();
+
+ List appTargets = mPredictionServicePredictions[mActiveClient.ordinal()];
+ if (!appTargets.isEmpty()) {
+ for (AppTarget appTarget : appTargets) {
+ ComponentKey key;
+ if (appTarget.getShortcutInfo() != null) {
+ key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
+ } else {
+ key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
+ appTarget.getClassName()), appTarget.getUser());
+ }
+ state.apps.add(new ComponentKeyMapper(mContext, key, mDynamicItemCache));
+ }
+ }
+ updateDependencies(state);
+ return state;
+ }
+
+ private void updateDependencies(PredictionState state) {
+ if (!state.isEnabled || mAppsView == null) {
+ return;
+ }
+
+ IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+ List instantAppsToLoad = new ArrayList<>();
+ List shortcutsToLoad = new ArrayList<>();
+ int total = state.apps.size();
+ for (int i = 0, count = 0; i < total && count < mMaxIconsPerRow; i++) {
+ ComponentKeyMapper mapper = state.apps.get(i);
+ // Update instant apps
+ if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
+ instantAppsToLoad.add(mapper.getPackage());
+ count++;
+ } else if (mapper.getComponentKey() instanceof ShortcutKey) {
+ shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
+ count++;
+ } else {
+ // Reload high res icon
+ AppInfo info = (AppInfo) mapper.getApp(mAppsView.getAppsStore());
+ if (info != null) {
+ if (info.usingLowResIcon()) {
+ // TODO: Update icon cache to support null callbacks.
+ iconCache.updateIconInBackground(this, info);
+ }
+ count++;
+ }
+ }
+ }
+ mDynamicItemCache.cacheItems(shortcutsToLoad, instantAppsToLoad);
+ }
+
+ @Override
+ public void onAppsUpdated() {
+ dispatchOnChange(false);
+ }
+
+ public boolean arePredictionsEnabled() {
+ return mCurrentState.isEnabled;
+ }
+
+ private boolean canApplyPredictions(PredictionState newState) {
+ if (mAppsView == null) {
+ // If there is no apps view, no need to schedule.
+ return true;
+ }
+ Launcher launcher = Launcher.getLauncher(mAppsView.getContext());
+ PredictionRowView predictionRow = mAppsView.getFloatingHeaderView().
+ findFixedRowByType(PredictionRowView.class);
+ if (!predictionRow.isShown() || predictionRow.getAlpha() == 0 ||
+ launcher.isForceInvisible()) {
+ return true;
+ }
+
+ if (mCurrentState.isEnabled != newState.isEnabled
+ || mCurrentState.apps.isEmpty() != newState.apps.isEmpty()) {
+ // If the visibility of the prediction row is changing, apply immediately.
+ return true;
+ }
+
+ if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+ // If we are here & mAppsView.isShown() = true, we are probably in all-apps or mid way
+ return false;
+ }
+ if (!launcher.isInState(OVERVIEW) && !launcher.isInState(BACKGROUND_APP)) {
+ // Just a fallback as we dont need to apply instantly, if we are not in the swipe-up UI
+ return false;
+ }
+
+ // Instead of checking against 1, we should check against (1 + delta), where delta accounts
+ // for the nav-bar height (as app icon can still be visible under the nav-bar). Checking
+ // against 1, keeps the logic simple :)
+ return launcher.getAllAppsController().getProgress() > 1;
+ }
+
+ public PredictionState getCurrentState() {
+ return mCurrentState;
+ }
+
+ public static class PredictionState {
+
+ public boolean isEnabled;
+ public List apps;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 0d5574fa91..11a1885d1a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.uioverrides;
+import static com.android.launcher3.LauncherState.RECENTS_CLEAR_ALL_BUTTON;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
import android.animation.ValueAnimator;
@@ -22,15 +24,17 @@ import android.annotation.TargetApi;
import android.os.Build;
import android.util.FloatProperty;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.LauncherStateManager.AnimationConfig;
import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.quickstep.views.ClearAllButton;
import com.android.quickstep.views.LauncherRecentsView;
import com.android.quickstep.views.RecentsView;
-import androidx.annotation.NonNull;
-
/**
* State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing
* the basic view properties, this class also manages changes in the task visuals.
@@ -49,10 +53,8 @@ public final class RecentsViewStateController extends
if (state.overviewUi) {
mRecentsView.updateEmptyMessage();
mRecentsView.resetTaskVisuals();
- mRecentsView.setHintVisibility(1);
- } else {
- mRecentsView.setHintVisibility(0);
}
+ setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state.getVisibleElements(mLauncher));
}
@Override
@@ -62,7 +64,6 @@ public final class RecentsViewStateController extends
if (!toState.overviewUi) {
builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals);
- mRecentsView.setHintVisibility(0);
}
if (toState.overviewUi) {
@@ -74,8 +75,15 @@ public final class RecentsViewStateController extends
updateAnim.setDuration(config.duration);
builder.play(updateAnim);
mRecentsView.updateEmptyMessage();
- builder.addOnFinishRunnable(() -> mRecentsView.setHintVisibility(1));
}
+
+ setAlphas(config.getPropertySetter(builder), toState.getVisibleElements(mLauncher));
+ }
+
+ private void setAlphas(PropertySetter propertySetter, int visibleElements) {
+ boolean hasClearAllButton = (visibleElements & RECENTS_CLEAR_ALL_BUTTON) != 0;
+ propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
+ hasClearAllButton ? 1f : 0f, LINEAR);
}
@Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 462e63098c..140e45c514 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -18,11 +18,10 @@ package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
import android.os.RemoteException;
+
import com.android.launcher3.Launcher;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.quickstep.RecentsModel;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -75,4 +74,8 @@ public class BackgroundAppState extends OverviewState {
return new ScaleAndTranslation(scale, 0f, 0f);
}
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return super.getVisibleElements(launcher) & ~RECENTS_CLEAR_ALL_BUTTON;
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 94c1545745..cda03dcde1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -17,8 +17,6 @@ package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.logging.LoggerUtils.getTargetStr;
import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
@@ -27,7 +25,6 @@ import android.view.View;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
@@ -73,6 +70,15 @@ public class OverviewState extends LauncherState {
return new ScaleAndTranslation(scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor);
}
+ @Override
+ public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
+ if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
+ // If the hotseat icons are visible in overview, keep them in their normal position.
+ return super.getWorkspaceScaleAndTranslation(launcher);
+ }
+ return getWorkspaceScaleAndTranslation(launcher);
+ }
+
@Override
public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
return new ScaleAndTranslation(1f, 0f, 0f);
@@ -110,9 +116,9 @@ public class OverviewState extends LauncherState {
@Override
public int getVisibleElements(Launcher launcher) {
if (launcher.getDeviceProfile().isVerticalBarLayout()) {
- return VERTICAL_SWIPE_INDICATOR;
+ return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON;
} else {
- return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR |
+ return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON |
(launcher.getAppsView().getFloatingHeaderView().hasVisibleContent()
? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
index 6dd5e21a85..63581092b9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
@@ -28,6 +28,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
+import android.view.ViewConfiguration;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
@@ -44,12 +45,14 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
private static final long PEEK_ANIM_DURATION = 100;
private final MotionPauseDetector mMotionPauseDetector;
+ private final float mMotionPauseMinDisplacement;
private AnimatorSet mPeekAnim;
public FlingAndHoldTouchController(Launcher l) {
super(l, false /* allowDragToOverview */);
mMotionPauseDetector = new MotionPauseDetector(l);
+ mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
}
@Override
@@ -98,7 +101,8 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
@Override
public boolean onDrag(float displacement, MotionEvent event) {
- mMotionPauseDetector.addPosition(displacement, 0, event.getEventTime());
+ mMotionPauseDetector.setDisallowPause(-displacement < mMotionPauseMinDisplacement);
+ mMotionPauseDetector.addPosition(displacement, event.getEventTime());
return super.onDrag(displacement, event);
}
@@ -111,6 +115,7 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
AnimatorSetBuilder builder = new AnimatorSetBuilder();
builder.setInterpolator(AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
+ builder.setInterpolator(AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE, OVERSHOOT_1_2);
AnimatorSet overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
NORMAL, OVERVIEW, builder, ANIM_ALL, ATOMIC_DURATION);
overviewAnim.addListener(new AnimatorListenerAdapter() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 21ddfc0154..a6c4445619 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -17,8 +17,6 @@ package com.android.launcher3.uioverrides.touchcontrollers;
import static android.view.View.TRANSLATION_X;
-import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
-import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -44,9 +42,9 @@ import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.AnimatorSetBuilder;
import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.touch.SwipeDetector;
import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Command;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
import com.android.launcher3.util.TouchController;
import com.android.quickstep.views.RecentsView;
@@ -212,11 +210,13 @@ public class NavBarToHomeTouchController implements TouchController, SwipeDetect
private void onSwipeInteractionCompleted(LauncherState targetState) {
clearState();
mLauncher.getStateManager().goToState(targetState, false /* animated */);
+ AccessibilityManagerCompat.sendStateEventToTest(mLauncher, targetState.ordinal);
}
private void logStateChange(int startContainerType, int logAction) {
mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
LauncherLogProto.Action.Direction.UP,
+ mSwipeDetector.getDownX(), mSwipeDetector.getDownY(),
LauncherLogProto.ContainerType.NAVBAR,
startContainerType,
mEndState.containerType,
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
index 5337c39eee..20a2487349 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -75,7 +75,8 @@ public final class PortraitOverviewStateTouchHelper {
* @return the animation
*/
PendingAnimation createSwipeDownToTaskAppAnimation(long duration) {
- TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getNextPage());
+ mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
+ TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getCurrentPage());
if (taskView == null) {
throw new IllegalStateException("There is no task view to animate to.");
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
index ad916be30d..a76ecd53b1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
@@ -21,10 +21,11 @@ import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE_NOOP;
+
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction.UPLEFT;
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction.UPRIGHT;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE_NOOP;
import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.NAVBAR;
import android.animation.ValueAnimator;
@@ -35,12 +36,15 @@ import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
+import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.quickstep.util.MotionPauseDetector;
import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.launcher3.R;
+import com.android.systemui.shared.system.InputMonitorCompat;
import com.android.systemui.shared.system.NavigationBarCompat;
/**
@@ -76,27 +80,32 @@ public class AssistantTouchConsumer implements InputConsumer {
private float mLastProgress;
private int mState;
private int mDirection;
+ private ActivityControlHelper mActivityControlHelper;
private final float mDistThreshold;
private final long mTimeThreshold;
private final int mAngleThreshold;
private final float mSlop;
- private final MotionPauseDetector mMotionPauseDetector;
private final ISystemUiProxy mSysUiProxy;
private final InputConsumer mConsumerDelegate;
private final Context mContext;
+ private final InputMonitorCompat mInputMonitorCompat;
+
+
public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy,
- InputConsumer delegate) {
+ InputConsumer delegate, InputMonitorCompat inputMonitorCompat,
+ ActivityControlHelper activityControlHelper) {
final Resources res = context.getResources();
mContext = context;
mSysUiProxy = systemUiProxy;
mConsumerDelegate = delegate;
- mMotionPauseDetector = new MotionPauseDetector(context);
mDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold);
mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
- mSlop = NavigationBarCompat.getQuickScrubTouchSlopPx();
+ mSlop = NavigationBarCompat.getQuickStepDragSlopPx();
+ mInputMonitorCompat = inputMonitorCompat;
+ mActivityControlHelper = activityControlHelper;
mState = STATE_INACTIVE;
}
@@ -106,8 +115,18 @@ public class AssistantTouchConsumer implements InputConsumer {
}
@Override
- public boolean isActive() {
- return mState != STATE_INACTIVE;
+ public boolean useSharedSwipeState() {
+ if (mConsumerDelegate != null) {
+ return mConsumerDelegate.useSharedSwipeState();
+ }
+ return false;
+ }
+
+ @Override
+ public void onConsumerAboutToBeSwitched() {
+ if (mConsumerDelegate != null) {
+ mConsumerDelegate.onConsumerAboutToBeSwitched();
+ }
}
@Override
@@ -120,14 +139,6 @@ public class AssistantTouchConsumer implements InputConsumer {
mDownPos.set(ev.getX(), ev.getY());
mLastPos.set(mDownPos);
mTimeFraction = 0;
-
- // Detect when the gesture decelerates to start the assistant
- mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
- if (isPaused && mState == STATE_ASSISTANT_ACTIVE) {
- mTimeFraction = 1;
- updateAssistantProgress();
- }
- });
break;
}
case ACTION_POINTER_UP: {
@@ -156,6 +167,10 @@ public class AssistantTouchConsumer implements InputConsumer {
if (!mPassedSlop) {
// Normal gesture, ensure we pass the slop before we start tracking the gesture
if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) > mSlop) {
+
+ // Cancel touches to other windows (intercept)
+ mInputMonitorCompat.pilferPointers();
+
mPassedSlop = true;
mStartDragPos.set(mLastPos.x, mLastPos.y);
mDragTime = SystemClock.uptimeMillis();
@@ -165,7 +180,8 @@ public class AssistantTouchConsumer implements InputConsumer {
Math.atan2(mDownPos.y - mLastPos.y, mDownPos.x - mLastPos.x));
mDirection = angle > 90 ? UPLEFT : UPRIGHT;
angle = angle > 90 ? 180 - angle : angle;
- if (angle > mAngleThreshold) {
+
+ if (angle > mAngleThreshold && angle < 90) {
mState = STATE_ASSISTANT_ACTIVE;
if (mConsumerDelegate != null) {
@@ -183,7 +199,6 @@ public class AssistantTouchConsumer implements InputConsumer {
// Movement
mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
mLastPos.y - mStartDragPos.y);
- mMotionPauseDetector.addPosition(mDistance, 0, ev.getEventTime());
if (mDistance >= 0) {
final long diff = SystemClock.uptimeMillis() - mDragTime;
mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
@@ -212,7 +227,8 @@ public class AssistantTouchConsumer implements InputConsumer {
animator.setInterpolator(Interpolators.DEACCEL_2);
animator.start();
}
- mMotionPauseDetector.clear();
+ mPassedSlop = false;
+ mState = STATE_INACTIVE;
break;
}
@@ -232,6 +248,14 @@ public class AssistantTouchConsumer implements InputConsumer {
SWIPE, mDirection, NAVBAR);
Bundle args = new Bundle();
args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
+
+ BaseDraggingActivity launcherActivity = mActivityControlHelper.getCreatedActivity();
+ if (launcherActivity != null) {
+ launcherActivity.getRootView().
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+
mSysUiProxy.startAssistant(args);
mLaunchedAssistant = true;
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/DeviceLockedInputConsumer.java
index d919a3e711..7fd09f7dee 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/DeviceLockedInputConsumer.java
@@ -17,7 +17,9 @@ package com.android.quickstep;
import android.content.Context;
import android.content.Intent;
+import android.graphics.PointF;
import android.view.MotionEvent;
+import android.view.ViewConfiguration;
/**
* A dummy input consumer used when the device is still locked, e.g. from secure camera.
@@ -25,9 +27,13 @@ import android.view.MotionEvent;
public class DeviceLockedInputConsumer implements InputConsumer {
private final Context mContext;
+ private final float mTouchSlopSquared;
+ private final PointF mTouchDown = new PointF();
public DeviceLockedInputConsumer(Context context) {
mContext = context;
+ float touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mTouchSlopSquared = touchSlop * touchSlop;
}
@Override
@@ -37,11 +43,19 @@ public class DeviceLockedInputConsumer implements InputConsumer {
@Override
public void onMotionEvent(MotionEvent ev) {
- // For now, just start the home intent so user is prompted to unlock the device.
+ float x = ev.getX();
+ float y = ev.getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mContext.startActivity(new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ mTouchDown.set(x, y);
+ } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
+ float xSquared = (x - mTouchDown.x) * (x - mTouchDown.x);
+ float ySquared = (y - mTouchDown.y) * (y - mTouchDown.y);
+ if (xSquared + ySquared > mTouchSlopSquared) {
+ // For now, just start the home intent so user is prompted to unlock the device.
+ mContext.startActivity(new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_HOME)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
}
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
index 21e98f260b..f12efc83cd 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
@@ -26,11 +26,13 @@ import android.content.Context;
import android.graphics.Rect;
import android.graphics.RectF;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.util.RemoteAnimationTargetSet;
import com.android.quickstep.views.RecentsView;
@@ -39,9 +41,6 @@ import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
/**
* {@link ActivityControlHelper} for recents when the default launcher is different than the
* currently running one and apps should interact with the {@link RecentsActivity} as opposed
@@ -72,7 +71,9 @@ public final class FallbackActivityControllerHelper implements
@Override
public void onSwipeUpComplete(RecentsActivity activity) {
- // TODO:
+ RecentsView recentsView = activity.getOverviewPanel();
+ recentsView.getClearAllButton().setVisibilityAlpha(1);
+ recentsView.setDisallowScrollToClearAll(false);
}
@Override
@@ -121,6 +122,8 @@ public final class FallbackActivityControllerHelper implements
RecentsView rv = activity.getOverviewPanel();
rv.setContentAlpha(0);
+ rv.getClearAllButton().setVisibilityAlpha(0);
+ rv.setDisallowScrollToClearAll(true);
return new AnimationFactory() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/InputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/InputConsumer.java
index ad9fe78f85..e3f9e02a02 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/InputConsumer.java
@@ -34,7 +34,7 @@ public interface InputConsumer {
int getType();
- default boolean isActive() {
+ default boolean useSharedSwipeState() {
return false;
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index 621d1b07fa..35783b58a1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -38,10 +38,14 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherInitListener;
+import com.android.launcher3.LauncherInitListenerEx;
import com.android.launcher3.LauncherState;
import com.android.launcher3.allapps.DiscoveryBounce;
import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -59,10 +63,6 @@ import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
/**
* {@link ActivityControlHelper} for the in-launcher recents.
*/
@@ -163,21 +163,16 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
}
activity.getStateManager().setRestState(resetState);
- final LauncherState fromState;
- if (!activityVisible) {
- // Since the launcher is not visible, we can safely reset the scroll position.
- // This ensures then the next swipe up to all-apps starts from scroll 0.
- activity.getAppsView().reset(false /* animate */);
- fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
- activity.getStateManager().goToState(fromState, false);
+ final LauncherState fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
+ activity.getStateManager().goToState(fromState, false);
+ // Since all apps is not visible, we can safely reset the scroll position.
+ // This ensures then the next swipe up to all-apps starts from scroll 0.
+ activity.getAppsView().reset(false /* animate */);
- // Optimization, hide the all apps view to prevent layout while initializing
- activity.getAppsView().getContentView().setVisibility(View.GONE);
+ // Optimization, hide the all apps view to prevent layout while initializing
+ activity.getAppsView().getContentView().setVisibility(View.GONE);
- AccessibilityManagerCompat.sendStateEventToTest(activity, fromState.ordinal);
- } else {
- fromState = startState;
- }
+ AccessibilityManagerCompat.sendStateEventToTest(activity, fromState.ordinal);
return new AnimationFactory() {
private Animator mShelfAnim;
@@ -185,8 +180,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
@Override
public void createActivityController(long transitionLength) {
- createActivityControllerInternal(activity, activityVisible, fromState,
- transitionLength, callback);
+ createActivityControllerInternal(activity, fromState, transitionLength, callback);
}
@Override
@@ -230,18 +224,9 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
};
}
- private void createActivityControllerInternal(Launcher activity, boolean wasVisible,
- LauncherState fromState, long transitionLength,
- Consumer callback) {
+ private void createActivityControllerInternal(Launcher activity, LauncherState fromState,
+ long transitionLength, Consumer callback) {
LauncherState endState = OVERVIEW;
- if (wasVisible && fromState != BACKGROUND_APP) {
- // If a translucent app was launched fom launcher, animate launcher states.
- DeviceProfile dp = activity.getDeviceProfile();
- long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
- callback.accept(activity.getStateManager()
- .createAnimationToNewWorkspace(fromState, endState, accuracy));
- return;
- }
if (fromState == endState) {
return;
}
@@ -319,7 +304,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
@Override
public ActivityInitListener createActivityInitListener(
BiPredicate onInitListener) {
- return new LauncherInitListener(onInitListener);
+ return new LauncherInitListenerEx(onInitListener);
}
@Nullable
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
index 990bcff51e..44ba515d76 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
@@ -21,9 +21,9 @@ import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.MotionEvent.INVALID_POINTER_ID;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
import static com.android.launcher3.util.RaceConditionTracker.ENTER;
import static com.android.launcher3.util.RaceConditionTracker.EXIT;
-import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
@@ -45,6 +45,9 @@ import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.WindowManager;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.R;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RaceConditionTracker;
import com.android.launcher3.util.TraceHelper;
@@ -62,8 +65,6 @@ import com.android.systemui.shared.system.WindowManagerWrapper;
import java.util.function.Consumer;
-import androidx.annotation.UiThread;
-
/**
* Input consumer for handling events originating from an activity other than Launcher
*/
@@ -90,6 +91,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
private final Consumer mOnCompleteCallback;
private final MotionPauseDetector mMotionPauseDetector;
+ private final float mMotionPauseMinDisplacement;
private VelocityTracker mVelocityTracker;
private WindowTransformSwipeHandler mInteractionHandler;
@@ -107,8 +109,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
// Slop used to determine when we say that the gesture has started.
private boolean mPassedTouchSlop;
- // TODO: Start displacement should have both x and y
+ // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
private float mStartDisplacement;
+ private float mStartDisplacementX;
private Handler mMainThreadHandler;
private Runnable mCancelRecentsAnimationRunnable = () -> {
@@ -131,6 +134,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
mMode = SysUINavigationMode.getMode(base);
mMotionPauseDetector = new MotionPauseDetector(base);
+ mMotionPauseMinDisplacement = base.getResources().getDimension(
+ R.dimen.motion_pause_detector_min_displacement_from_app);
mOnCompleteCallback = onCompleteCallback;
mVelocityTracker = VelocityTracker.obtain();
mInputMonitorCompat = inputMonitorCompat;
@@ -149,6 +154,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx();
mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx();
+
mPassedTouchSlop = mPassedDragSlop = continuingPreviousGesture;
}
@@ -218,6 +224,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
}
mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
float displacement = getDisplacement(ev);
+ float displacementX = mLastPos.x - mDownPos.x;
if (!mPassedDragSlop) {
if (!mIsDeferredDownTarget) {
@@ -226,13 +233,13 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
if (Math.abs(displacement) > mDragSlop) {
mPassedDragSlop = true;
mStartDisplacement = displacement;
+ mStartDisplacementX = displacementX;
}
}
}
if (!mPassedTouchSlop) {
- if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) >=
- mTouchSlop) {
+ if (Math.hypot(displacementX, mLastPos.y - mDownPos.y) >= mTouchSlop) {
mPassedTouchSlop = true;
if (mIsDeferredDownTarget) {
@@ -243,6 +250,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
if (!mPassedDragSlop) {
mPassedDragSlop = true;
mStartDisplacement = displacement;
+ mStartDisplacementX = displacementX;
}
notifyGestureStarted();
}
@@ -253,12 +261,12 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
if (mMode == Mode.NO_BUTTON) {
- boolean isLandscape = isNavBarOnLeft() || isNavBarOnRight();
- float orthogonalDisplacement = !isLandscape
- ? ev.getX() - mDownPos.x
- : ev.getY() - mDownPos.y;
- mMotionPauseDetector.addPosition(displacement, orthogonalDisplacement,
- ev.getEventTime());
+ float horizontalDist = Math.abs(displacementX - mStartDisplacementX);
+ float upDist = -(displacement - mStartDisplacement);
+ boolean isLikelyToStartNewTask = horizontalDist > upDist;
+ mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
+ || isLikelyToStartNewTask);
+ mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
}
}
break;
@@ -346,7 +354,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
: velocityY;
mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
- mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY));
+ mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY),
+ mDownPos);
} else {
// Since we start touch tracking on DOWN, we may reach this state without actually
// starting the gesture. In that case, just cleanup immediately.
@@ -412,7 +421,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
}
@Override
- public boolean isActive() {
+ public boolean useSharedSwipeState() {
return mInteractionHandler != null;
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index d979c991a2..84af109a3b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -36,6 +36,8 @@ import java.util.List;
*/
public class TaskOverlayFactory implements ResourceBasedOverride {
+ public static final String AIAI_PACKAGE = "com.google.android.as";
+
/** Note that these will be shown in order from top to bottom, if available for the task. */
private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{
new TaskSystemShortcut.AppInfo(),
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
index 6e98a5ad2f..e3dcadcd54 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
@@ -38,7 +38,7 @@ import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
-import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.InstantAppResolver;
@@ -86,7 +86,7 @@ public class TaskSystemShortcut extends SystemShortcut
public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) {
Task task = view.getTask();
- ShortcutInfo dummyInfo = new ShortcutInfo();
+ WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
dummyInfo.intent = new Intent();
ComponentName component = task.getTopComponent();
dummyInfo.intent.setComponent(component);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 6044b61e1a..fc3f332d5d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -87,7 +87,7 @@ public class TouchInteractionService extends Service implements
new LooperExecutor(UiThreadHelper.getBackgroundLooper());
private static final String NAVBAR_VERTICAL_SIZE = "navigation_bar_frame_height";
- private static final String NAVBAR_HORIZONTAL_SIZE = "navigation_bar_frame_width";
+ private static final String NAVBAR_HORIZONTAL_SIZE = "navigation_bar_width";
public static final EventLogArray TOUCH_INTERACTION_LOG =
new EventLogArray("touch_interaction_log", 40);
@@ -422,7 +422,7 @@ public class TouchInteractionService extends Service implements
if (event.getAction() == ACTION_DOWN) {
if (isInValidSystemUiState()
&& mSwipeTouchRegion.contains(event.getX(), event.getY())) {
- boolean useSharedState = mConsumer.isActive();
+ boolean useSharedState = mConsumer.useSharedSwipeState();
mConsumer.onConsumerAboutToBeSwitched();
mConsumer = newConsumer(useSharedState, event);
TOUCH_INTERACTION_LOG.addLog("setInputConsumer", mConsumer.getType());
@@ -455,13 +455,18 @@ public class TouchInteractionService extends Service implements
final ActivityControlHelper activityControl =
mOverviewComponentObserver.getActivityControlHelper();
+
if (runningTaskInfo == null && !mSwipeSharedState.goingToLauncher) {
return InputConsumer.NO_OP;
} else if (mAssistantAvailable
&& SysUINavigationMode.INSTANCE.get(this).getMode() == Mode.NO_BUTTON
&& AssistantTouchConsumer.withinTouchRegion(this, event)) {
- return new AssistantTouchConsumer(this, mISystemUiProxy, !activityControl.isResumed()
- ? createOtherActivityInputConsumer(event, runningTaskInfo) : null);
+
+ boolean addDelegate = !activityControl.isResumed();
+ return new AssistantTouchConsumer(this, mISystemUiProxy, addDelegate ?
+ createOtherActivityInputConsumer(event, runningTaskInfo) : null,
+ mInputMonitorCompat, activityControl);
+
} else if (mSwipeSharedState.goingToLauncher || activityControl.isResumed()) {
return OverviewInputConsumer.newInstance(activityControl, false);
} else if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 5a1d3877a4..e1b38b3336 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -64,6 +64,9 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.Interpolator;
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
@@ -103,9 +106,6 @@ import com.android.systemui.shared.system.WindowCallbacksCompat;
import java.util.function.BiFunction;
import java.util.function.Consumer;
-import androidx.annotation.NonNull;
-import androidx.annotation.UiThread;
-
@TargetApi(Build.VERSION_CODES.O)
public class WindowTransformSwipeHandler
implements SwipeAnimationListener, OnApplyWindowInsetsListener {
@@ -253,6 +253,7 @@ public class WindowTransformSwipeHandler
private boolean mGestureStarted;
private int mLogAction = Touch.SWIPE;
private int mLogDirection = Direction.UP;
+ private PointF mDownPos;
private final RecentsAnimationWrapper mRecentsAnimationWrapper;
@@ -429,8 +430,19 @@ public class WindowTransformSwipeHandler
// If we've already ended the gesture and are going home, don't prepare recents UI,
// as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
if (mGestureEndTarget != HOME) {
- mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
- mWasLauncherAlreadyVisible, true, this::onAnimatorPlaybackControllerCreated);
+ Runnable initAnimFactory = () -> {
+ mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
+ mWasLauncherAlreadyVisible, true,
+ this::onAnimatorPlaybackControllerCreated);
+ };
+ if (mWasLauncherAlreadyVisible) {
+ // Launcher is visible, but might be about to stop. Thus, if we prepare recents
+ // now, it might get overridden by moveToRestState() in onStop(). To avoid this,
+ // wait until the next gesture (and possibly launcher) starts.
+ mStateCallback.addCallback(STATE_GESTURE_STARTED, initAnimFactory);
+ } else {
+ initAnimFactory.run();
+ }
}
AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible);
@@ -692,9 +704,10 @@ public class WindowTransformSwipeHandler
/**
* @param endVelocity The velocity in the direction of the nav bar to the middle of the screen.
* @param velocity The x and y components of the velocity when the gesture ends.
+ * @param downPos The x and y value of where the gesture started.
*/
@UiThread
- public void onGestureEnded(float endVelocity, PointF velocity) {
+ public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
float flingThreshold = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_velocity);
boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
@@ -707,6 +720,7 @@ public class WindowTransformSwipeHandler
} else {
mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT;
}
+ mDownPos = downPos;
handleNormalGestureEnd(endVelocity, isFling, velocity);
}
@@ -733,14 +747,10 @@ public class WindowTransformSwipeHandler
float endShift;
final float startShift;
Interpolator interpolator = DEACCEL;
- int nextPage = 0;
- int taskToLaunch = 0;
final boolean goingToNewTask;
if (mRecentsView != null) {
- nextPage = mRecentsView.getNextPage();
- final int lastTaskIndex = mRecentsView.getTaskViewCount() - 1;
final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
- taskToLaunch = nextPage <= lastTaskIndex ? nextPage : lastTaskIndex;
+ final int taskToLaunch = mRecentsView.getNextPage();
goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
} else {
goingToNewTask = false;
@@ -806,11 +816,6 @@ public class WindowTransformSwipeHandler
}
}
- if (mRecentsView != null && !endTarget.isLauncher && taskToLaunch != nextPage) {
- // Scrolled to Clear all button, snap back to last task and launch it.
- mRecentsView.snapToPage(taskToLaunch, Math.toIntExact(duration), interpolator);
- }
-
if (endTarget == HOME) {
setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
@@ -845,6 +850,7 @@ public class WindowTransformSwipeHandler
: mRecentsView.getNextPage();
UserEventDispatcher.newInstance(mContext).logStateChangeAction(
mLogAction, mLogDirection,
+ (int) mDownPos.x, (int) mDownPos.y,
ContainerType.NAVBAR, ContainerType.APP,
endTarget.containerType,
pageIndex);
@@ -1044,7 +1050,12 @@ public class WindowTransformSwipeHandler
}
private void invalidateHandlerWithLauncher() {
- mLauncherTransitionController = null;
+ if (mLauncherTransitionController != null) {
+ if (mLauncherTransitionController.getAnimationPlayer().isStarted()) {
+ mLauncherTransitionController.getAnimationPlayer().cancel();
+ }
+ mLauncherTransitionController = null;
+ }
mRecentsView.setEnableFreeScroll(true);
mRecentsView.setRunningTaskIconScaledDown(false);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/hints/ChipsContainer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/hints/ChipsContainer.java
deleted file mode 100644
index 8fc89f2538..0000000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/hints/ChipsContainer.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2019 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.quickstep.hints;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.view.View;
-import android.widget.FrameLayout;
-
-public class ChipsContainer extends FrameLayout {
-
- private static final String TAG = "ChipsContainer";
-
- public static final FloatProperty HINT_VISIBILITY =
- new FloatProperty("hint_visibility") {
- @Override
- public void setValue(ChipsContainer chipsContainer, float v) {
- chipsContainer.setHintVisibility(v);
- }
-
- @Override
- public Float get(ChipsContainer chipsContainer) {
- return chipsContainer.mHintVisibility;
- }
- };
-
- private float mHintVisibility;
-
- public ChipsContainer(Context context) {
- super(context);
- }
-
- public ChipsContainer(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public ChipsContainer(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public ChipsContainer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- public void setView(View v) {
- removeAllViews();
- addView(v);
- }
-
- public void setHintVisibility(float v) {
- if (v == 1) {
- setVisibility(VISIBLE);
- } else {
- setVisibility(GONE);
- }
- mHintVisibility = v;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
index 32d510dd28..71f33c3fef 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -102,7 +102,7 @@ public class ClipAnimationHelper {
public ClipAnimationHelper(Context context) {
mWindowCornerRadius = getWindowCornerRadius(context.getResources());
mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources());
- mTaskCornerRadius = Themes.getDialogCornerRadius(context);
+ mTaskCornerRadius = TaskCornerRadius.get(context);
}
private void updateSourceStack(RemoteAnimationTargetCompat target) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java
new file mode 100644
index 0000000000..3ddf1b60f3
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 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.quickstep.util;
+
+import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
+
+import android.content.Context;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+public class TaskCornerRadius {
+
+ public static float get(Context context) {
+ return supportsRoundedCornersOnWindows(context.getResources()) ?
+ Themes.getDialogCornerRadius(context):
+ context.getResources().getDimension(R.dimen.task_corner_radius_small);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
index fbecd8486a..9db0c09ae2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
@@ -18,6 +18,7 @@ package com.android.quickstep.views;
import android.content.Context;
import android.util.AttributeSet;
+import android.util.Property;
import android.widget.Button;
import com.android.launcher3.Utilities;
@@ -26,8 +27,22 @@ import com.android.quickstep.views.RecentsView.ScrollState;
public class ClearAllButton extends Button implements PageCallbacks {
+ public static final Property VISIBILITY_ALPHA =
+ new Property(Float.class, "visibilityAlpha") {
+ @Override
+ public Float get(ClearAllButton view) {
+ return view.mVisibilityAlpha;
+ }
+
+ @Override
+ public void set(ClearAllButton view, Float visibilityAlpha) {
+ view.setVisibilityAlpha(visibilityAlpha);
+ }
+ };
+
private float mScrollAlpha = 1;
private float mContentAlpha = 1;
+ private float mVisibilityAlpha = 1;
private final boolean mIsRtl;
@@ -58,6 +73,13 @@ public class ClearAllButton extends Button implements PageCallbacks {
}
}
+ public void setVisibilityAlpha(float alpha) {
+ if (mVisibilityAlpha != alpha) {
+ mVisibilityAlpha = alpha;
+ updateAlpha();
+ }
+ }
+
@Override
public void onPageScroll(ScrollState scrollState) {
float width = getWidth();
@@ -72,7 +94,7 @@ public class ClearAllButton extends Button implements PageCallbacks {
}
private void updateAlpha() {
- final float alpha = mScrollAlpha * mContentAlpha;
+ final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha;
setAlpha(alpha);
setClickable(alpha == 1);
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 446fb39055..204dd56872 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -32,7 +32,6 @@ import android.os.UserHandle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
-import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -59,7 +58,6 @@ public final class DigitalWellBeingToast extends LinearLayout {
private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
private Task mTask;
- private ImageView mImage;
private TextView mText;
public DigitalWellBeingToast(Context context, AttributeSet attrs) {
@@ -70,12 +68,15 @@ public final class DigitalWellBeingToast extends LinearLayout {
mLauncherApps = context.getSystemService(LauncherApps.class);
}
+ public TextView getTextView() {
+ return mText;
+ }
+
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mText = findViewById(R.id.digital_well_being_remaining_time);
- mImage = findViewById(R.id.digital_well_being_hourglass);
}
public void initialize(Task task, InitializeCallback callback) {
@@ -103,8 +104,6 @@ public final class DigitalWellBeingToast extends LinearLayout {
} else {
setVisibility(VISIBLE);
mText.setText(getText(appRemainingTimeMs));
- mImage.setImageResource(appRemainingTimeMs > 0 ?
- R.drawable.hourglass_top : R.drawable.hourglass_bottom);
}
callback.call(getContentDescriptionForTask(
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 1ccc3f16df..d6f2235031 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -19,9 +19,9 @@ import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.RECENTS_CLEAR_ALL_BUTTON;
import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_HINTS_IN_OVERVIEW;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import android.animation.AnimatorSet;
@@ -39,11 +39,10 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.util.PendingAnimation;
-import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
import com.android.launcher3.views.ScrimView;
import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.hints.ChipsContainer;
import com.android.quickstep.util.ClipAnimationHelper;
import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
import com.android.quickstep.util.LayoutUtils;
@@ -55,7 +54,6 @@ import com.android.quickstep.util.LayoutUtils;
public class LauncherRecentsView extends RecentsView {
private final TransformParams mTransformParams = new TransformParams();
- private ChipsContainer mChipsContainer;
public LauncherRecentsView(Context context) {
this(context, null);
@@ -75,14 +73,6 @@ public class LauncherRecentsView extends RecentsView {
mActivity.getStateManager().goToState(NORMAL);
}
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mChipsContainer = mActivity.findViewById(R.id.hints);
- BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) mChipsContainer.getLayoutParams();
- params.bottomMargin = mActivity.getDeviceProfile().chipHintBottomMarginPx;
- }
-
@Override
public void setTranslationY(float translationY) {
super.setTranslationY(translationY);
@@ -94,16 +84,6 @@ public class LauncherRecentsView extends RecentsView {
}
}
- public void setHintVisibility(float v) {
- if (mChipsContainer != null && ENABLE_HINTS_IN_OVERVIEW.get()) {
- mChipsContainer.setHintVisibility(v);
- }
- }
-
- public ChipsContainer getChipsContainer() {
- return mChipsContainer;
- }
-
@Override
public void draw(Canvas canvas) {
maybeDrawEmptyMessage(canvas);
@@ -154,37 +134,6 @@ public class LauncherRecentsView extends RecentsView {
return anim;
}
- @Override
- public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) {
- PendingAnimation anim = super.createTaskLauncherAnimation(tv, duration);
-
- if (ENABLE_HINTS_IN_OVERVIEW.get()) {
- anim.anim.play(ObjectAnimator.ofFloat(
- mChipsContainer, ChipsContainer.HINT_VISIBILITY, 0));
- }
-
- return anim;
- }
-
- @Override
- public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
- boolean shouldRemoveTask, long duration) {
- PendingAnimation anim = super.createTaskDismissAnimation(taskView, animateTaskView,
- shouldRemoveTask, duration);
-
- if (ENABLE_HINTS_IN_OVERVIEW.get()) {
- anim.anim.play(ObjectAnimator.ofFloat(
- mChipsContainer, ChipsContainer.HINT_VISIBILITY, 0));
- anim.addEndListener(onEndListener -> {
- if (!onEndListener.isSuccess) {
- mChipsContainer.setHintVisibility(1);
- }
- });
- }
-
- return anim;
- }
-
@Override
protected void getTaskSize(DeviceProfile dp, Rect outRect) {
LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
@@ -247,4 +196,24 @@ public class LauncherRecentsView extends RecentsView {
}
}
}
+
+ @Override
+ public void reset() {
+ super.reset();
+
+ // We are moving to home or some other UI with no recents. Switch back to the home client,
+ // the home predictions should have been updated when the activity was resumed.
+ PredictionUiStateManager.INSTANCE.get(getContext()).switchClient(Client.HOME);
+ }
+
+ @Override
+ public void setOverviewStateEnabled(boolean enabled) {
+ super.setOverviewStateEnabled(enabled);
+ if (enabled) {
+ LauncherState state = mActivity.getStateManager().getState();
+ boolean hasClearAllButton = (state.getVisibleElements(mActivity)
+ & RECENTS_CLEAR_ALL_BUTTON) != 0;
+ setDisallowScrollToClearAll(!hasClearAllButton);
+ }
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index ddb94d2dda..08a76166cf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -18,6 +18,7 @@ package com.android.quickstep.views;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
import static com.android.launcher3.anim.Interpolators.ACCEL;
import static com.android.launcher3.anim.Interpolators.ACCEL_2;
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
@@ -25,9 +26,10 @@ import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
-import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
import static com.android.quickstep.util.ClipAnimationHelper.TransformParams;
import android.animation.Animator;
@@ -46,6 +48,7 @@ import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
@@ -89,7 +92,6 @@ import com.android.launcher3.util.OverScroller;
import com.android.launcher3.util.PendingAnimation;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.ViewPool;
-import com.android.quickstep.OverviewCallbacks;
import com.android.quickstep.RecentsAnimationWrapper;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener;
@@ -169,6 +171,7 @@ public abstract class RecentsView extends PagedView impl
private final ViewPool mTaskViewPool;
private boolean mDwbToastShown;
+ private boolean mDisallowScrollToClearAll;
/**
* TODO: Call reloadIdNeeded in onTaskStackChanged.
@@ -322,6 +325,8 @@ public abstract class RecentsView extends PagedView impl
mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
mEmptyMessagePaint.setTextSize(getResources()
.getDimension(R.dimen.recents_empty_message_text_size));
+ mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
+ Typeface.NORMAL));
mEmptyMessagePadding = getResources()
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
setWillNotDraw(false);
@@ -748,8 +753,6 @@ public abstract class RecentsView extends PagedView impl
unloadVisibleTaskData();
setCurrentPage(0);
-
- OverviewCallbacks.get(getContext()).onResetOverview();
}
/**
@@ -1159,6 +1162,7 @@ public abstract class RecentsView extends PagedView impl
@SuppressWarnings("unused")
private void dismissAllTasks(View view) {
runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
+ mActivity.getUserEventDispatcher().logActionOnControl(TAP, CLEAR_ALL_BUTTON);
}
private void dismissCurrentTask() {
@@ -1606,4 +1610,33 @@ public abstract class RecentsView extends PagedView impl
mRecentsAnimationWrapper.finish(toRecents, onFinishComplete);
}
+
+ public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
+ if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
+ mDisallowScrollToClearAll = disallowScrollToClearAll;
+ updateMinAndMaxScrollX();
+ }
+ }
+
+ @Override
+ protected int computeMinScrollX() {
+ if (mIsRtl && mDisallowScrollToClearAll) {
+ // We aren't showing the clear all button, so use the leftmost task as the min scroll.
+ return getScrollForPage(getTaskViewCount() - 1);
+ }
+ return super.computeMinScrollX();
+ }
+
+ @Override
+ protected int computeMaxScrollX() {
+ if (!mIsRtl && mDisallowScrollToClearAll) {
+ // We aren't showing the clear all button, so use the rightmost task as the max scroll.
+ return getScrollForPage(getTaskViewCount() - 1);
+ }
+ return super.computeMaxScrollX();
+ }
+
+ public ClearAllButton getClearAllButton() {
+ return mClearAllButton;
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 7905230223..ed68d87d7e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -48,6 +48,7 @@ import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes;
import com.android.quickstep.TaskOverlayFactory;
import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
+import com.android.quickstep.util.TaskCornerRadius;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.QuickStepContract;
@@ -108,7 +109,7 @@ public class TaskThumbnailView extends View {
public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- mCornerRadius = Themes.getDialogCornerRadius(context);
+ mCornerRadius = TaskCornerRadius.get(context);
mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
mPaint.setFilterBitmap(true);
mBackgroundPaint.setColor(Color.WHITE);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 02a2ec6e2f..3ca8b18246 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -211,6 +211,10 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
return mMenuView;
}
+ public DigitalWellBeingToast getDigitalWellBeingToast() {
+ return mDigitalWellBeingToast;
+ }
+
/**
* Updates this task view to the given {@param task}.
*/
diff --git a/quickstep/res/drawable/hourglass_bottom.xml b/quickstep/res/drawable/hourglass_bottom.xml
deleted file mode 100644
index b5ef008d28..0000000000
--- a/quickstep/res/drawable/hourglass_bottom.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
diff --git a/quickstep/res/drawable/hourglass_top.xml b/quickstep/res/drawable/ic_hourglass_top.xml
similarity index 82%
rename from quickstep/res/drawable/hourglass_top.xml
rename to quickstep/res/drawable/ic_hourglass_top.xml
index 7fc77d3fc4..2f8bec381e 100644
--- a/quickstep/res/drawable/hourglass_top.xml
+++ b/quickstep/res/drawable/ic_hourglass_top.xml
@@ -2,7 +2,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
- android:viewportHeight="24">
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorPrimary">
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/textColorPrimary">
+ android:strokeColor="#FFFFFFFF"
+ android:strokeAlpha=".008"
+ android:pathData="M0 0h24v24H0z" />
+
+
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_split_screen.xml b/quickstep/res/drawable/ic_split_screen.xml
index 110af914c5..108006929c 100644
--- a/quickstep/res/drawable/ic_split_screen.xml
+++ b/quickstep/res/drawable/ic_split_screen.xml
@@ -17,7 +17,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
- android:viewportHeight="24">
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorPrimary">
200
- 30
+ 20
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 6ec3bf62d7..32f312fdbe 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -19,6 +19,8 @@
24dp
12dp
48dp
+
+ 2dp
10dp
70dp
@@ -36,7 +38,7 @@
0.0285dp
0.285dp
0.5dp
- 48dp
+ 36dp
50dp
@@ -63,8 +65,8 @@
24dp
- 28dp
- 70dp
+ 48dp
+ 55dp
28dp
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 81565a5720..4319b5d71d 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -58,4 +58,13 @@
%1$s left today
+
+
+ App suggestions
+
+ All apps
+
+ Your predicted apps
+
+
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index b263a4c963..4f50cdb614 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -87,7 +87,7 @@ public class UiFactory extends RecentsUiFactory {
OverviewInteractionState.INSTANCE.get(launcher)
.setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */);
if (launcher != null && launcher.getDragLayer() != null) {
- launcher.getDragLayer().setDisallowBackGesture(shouldBackButtonBeHidden);
+ launcher.getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
}
}
diff --git a/quickstep/src/com/android/quickstep/OverviewCallbacks.java b/quickstep/src/com/android/quickstep/OverviewCallbacks.java
index ef9c5c0d94..f5573baa7f 100644
--- a/quickstep/src/com/android/quickstep/OverviewCallbacks.java
+++ b/quickstep/src/com/android/quickstep/OverviewCallbacks.java
@@ -39,7 +39,5 @@ public class OverviewCallbacks implements ResourceBasedOverride {
public void onInitOverviewTransition() { }
- public void onResetOverview() { }
-
public void closeAllWindows() { }
}
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 724a492e3f..a2f07e318f 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -20,8 +20,8 @@ import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_CHANGED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
-import static com.android.systemui.shared.system.PackageManagerWrapper
- .ACTION_PREFERRED_ACTIVITY_CHANGED;
+import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
+import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -29,7 +29,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ResolveInfo;
-import android.os.PatternMatcher;
import com.android.systemui.shared.system.PackageManagerWrapper;
@@ -109,13 +108,9 @@ public final class OverviewComponentObserver {
}
mUpdateRegisteredPackage = defaultHome.getPackageName();
- IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED);
- updateReceiver.addAction(ACTION_PACKAGE_CHANGED);
- updateReceiver.addAction(ACTION_PACKAGE_REMOVED);
- updateReceiver.addDataScheme("package");
- updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage,
- PatternMatcher.PATTERN_LITERAL);
- mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver);
+ mContext.registerReceiver(mOtherHomeAppUpdateReceiver, getPackageFilter(
+ mUpdateRegisteredPackage, ACTION_PACKAGE_ADDED, ACTION_PACKAGE_CHANGED,
+ ACTION_PACKAGE_REMOVED));
}
}
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index dbae794e04..b67c6f8c8d 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -16,15 +16,15 @@
package com.android.quickstep;
+import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.res.Resources;
import android.util.Log;
import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.system.QuickStepContract;
import java.util.ArrayList;
import java.util.List;
@@ -70,8 +70,6 @@ public class SysUINavigationMode {
mContext = context;
initializeMode();
- IntentFilter filter = new IntentFilter(ACTION_OVERLAY_CHANGED);
- filter.addDataScheme("package");
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -81,7 +79,7 @@ public class SysUINavigationMode {
dispatchModeChange();
}
}
- }, filter);
+ }, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
}
private void initializeMode() {
diff --git a/quickstep/src/com/android/quickstep/TestInformationProvider.java b/quickstep/src/com/android/quickstep/TestInformationProvider.java
index b37ddda097..a948570d2a 100644
--- a/quickstep/src/com/android/quickstep/TestInformationProvider.java
+++ b/quickstep/src/com/android/quickstep/TestInformationProvider.java
@@ -111,6 +111,14 @@ public class TestInformationProvider extends ContentProvider {
response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) distance);
break;
}
+
+ case TestProtocol.REQUEST_ENABLE_DRAG_LOGGING:
+ TestProtocol.sDebugTracing = true;
+ break;
+
+ case TestProtocol.REQUEST_DISABLE_DRAG_LOGGING:
+ TestProtocol.sDebugTracing = false;
+ break;
}
return response;
}
diff --git a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
index 6dff187ea5..4a11601bbb 100644
--- a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
+++ b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
@@ -36,17 +36,19 @@ import com.android.systemui.shared.system.MetricsLoggerCompat;
@SuppressWarnings("unused")
public class UserEventDispatcherExtension extends UserEventDispatcher {
+ public static final int ALL_APPS_PREDICTION_TIPS = 2;
+
private static final String TAG = "UserEventDispatcher";
public UserEventDispatcherExtension(Context context) { }
- public void logStateChangeAction(int action, int dir, int srcChildTargetType,
- int srcParentContainerType, int dstContainerType,
- int pageIndex) {
+ public void logStateChangeAction(int action, int dir, int downX, int downY,
+ int srcChildTargetType, int srcParentContainerType,
+ int dstContainerType, int pageIndex) {
new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY,
dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER);
- super.logStateChangeAction(action, dir, srcChildTargetType, srcParentContainerType,
- dstContainerType, pageIndex);
+ super.logStateChangeAction(action, dir, downX, downY, srcChildTargetType,
+ srcParentContainerType, dstContainerType, pageIndex);
}
public void logActionTip(int actionType, int viewType) {
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 47f4f4d8fe..c8aed81918 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -43,8 +43,6 @@ public class LayoutUtils {
float extraSpace;
if (dp.isVerticalBarLayout()) {
extraSpace = 0;
- } else if (FeatureFlags.ENABLE_HINTS_IN_OVERVIEW.get()){
- extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx + dp.chipHintHeightPx;
} else {
extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx;
}
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index ae5f3908be..f58f0d4857 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -38,28 +38,26 @@ public class MotionPauseDetector {
private final float mSpeedVerySlow;
private final float mSpeedSomewhatFast;
private final float mSpeedFast;
- private final float mMinDisplacementForPause;
private final Alarm mForcePauseTimeout;
private Long mPreviousTime = null;
private Float mPreviousPosition = null;
private Float mPreviousVelocity = null;
- private TotalDisplacement mTotalDisplacement = new TotalDisplacement();
private Float mFirstPosition = null;
- private Float mFirstOrthogonalPosition = null;
private OnMotionPauseListener mOnMotionPauseListener;
private boolean mIsPaused;
// Bias more for the first pause to make it feel extra responsive.
private boolean mHasEverBeenPaused;
+ /** @see #setDisallowPause(boolean) */
+ private boolean mDisallowPause;
public MotionPauseDetector(Context context) {
Resources res = context.getResources();
mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
- mMinDisplacementForPause = res.getDimension(R.dimen.motion_pause_detector_min_displacement);
mForcePauseTimeout = new Alarm();
mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
}
@@ -72,29 +70,31 @@ public class MotionPauseDetector {
mOnMotionPauseListener = listener;
}
+ /**
+ * @param disallowPause If true, we will not detect any pauses until this is set to false again.
+ */
+ public void setDisallowPause(boolean disallowPause) {
+ mDisallowPause = disallowPause;
+ updatePaused(mIsPaused);
+ }
+
/**
* Computes velocity and acceleration to determine whether the motion is paused.
* @param position The x or y component of the motion being tracked.
- * @param orthogonalPosition The x or y component (opposite of {@param position}) of the motion.
*
* TODO: Use historical positions as well, e.g. {@link MotionEvent#getHistoricalY(int, int)}.
*/
- public void addPosition(float position, float orthogonalPosition, long time) {
+ public void addPosition(float position, long time) {
if (mFirstPosition == null) {
mFirstPosition = position;
}
- if (mFirstOrthogonalPosition == null) {
- mFirstOrthogonalPosition = orthogonalPosition;
- }
mForcePauseTimeout.setAlarm(FORCE_PAUSE_TIMEOUT);
if (mPreviousTime != null && mPreviousPosition != null) {
long changeInTime = Math.max(1, time - mPreviousTime);
float changeInPosition = position - mPreviousPosition;
float velocity = changeInPosition / changeInTime;
if (mPreviousVelocity != null) {
- mTotalDisplacement.set(Math.abs(position - mFirstPosition),
- Math.abs(orthogonalPosition - mFirstOrthogonalPosition));
- checkMotionPaused(velocity, mPreviousVelocity, mTotalDisplacement);
+ checkMotionPaused(velocity, mPreviousVelocity);
}
mPreviousVelocity = velocity;
}
@@ -102,8 +102,7 @@ public class MotionPauseDetector {
mPreviousPosition = position;
}
- private void checkMotionPaused(float velocity, float prevVelocity,
- TotalDisplacement totalDisplacement) {
+ private void checkMotionPaused(float velocity, float prevVelocity) {
float speed = Math.abs(velocity);
float previousSpeed = Math.abs(prevVelocity);
boolean isPaused;
@@ -125,16 +124,13 @@ public class MotionPauseDetector {
}
}
}
- boolean passedMinDisplacement = totalDisplacement.primary >= mMinDisplacementForPause;
- boolean isDisplacementOrthogonal = totalDisplacement.orthogonal > totalDisplacement.primary;
- if (!passedMinDisplacement || isDisplacementOrthogonal) {
- mForcePauseTimeout.cancelAlarm();
- isPaused = false;
- }
updatePaused(isPaused);
}
private void updatePaused(boolean isPaused) {
+ if (mDisallowPause) {
+ isPaused = false;
+ }
if (mIsPaused != isPaused) {
mIsPaused = isPaused;
if (mIsPaused) {
@@ -151,8 +147,6 @@ public class MotionPauseDetector {
mPreviousPosition = null;
mPreviousVelocity = null;
mFirstPosition = null;
- mFirstOrthogonalPosition = null;
- mTotalDisplacement.set(0, 0);
setOnMotionPauseListener(null);
mIsPaused = mHasEverBeenPaused = false;
mForcePauseTimeout.cancelAlarm();
@@ -165,18 +159,4 @@ public class MotionPauseDetector {
public interface OnMotionPauseListener {
void onMotionPauseChanged(boolean isPaused);
}
-
- /**
- * Contains the displacement from the first tracked position,
- * along both the primary and orthogonal axes.
- */
- private class TotalDisplacement {
- public float primary;
- public float orthogonal;
-
- public void set(float primaryDisplacement, float orthogonalDisplacement) {
- this.primary = primaryDisplacement;
- this.orthogonal = orthogonalDisplacement;
- }
- }
}
diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
new file mode 100644
index 0000000000..43f6039404
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright (C) 2019 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.quickstep;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.pm.LauncherActivityInfo;
+import android.os.Process;
+import android.view.View;
+import android.widget.ProgressBar;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.appprediction.PredictionRowView;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.model.AppLaunchTracker;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class AppPredictionsUITests extends AbstractQuickStepTest {
+ private static final int DEFAULT_APP_LAUNCH_TIMES = 3;
+ private static final String TAG = "AppPredictionsUITests";
+
+ private LauncherActivityInfo mSampleApp1;
+ private LauncherActivityInfo mSampleApp2;
+ private LauncherActivityInfo mSampleApp3;
+
+ private AppPredictor.Callback mCallback;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ List activities = LauncherAppsCompat.getInstance(mTargetContext)
+ .getActivityList(null, Process.myUserHandle());
+ mSampleApp1 = activities.get(0);
+ mSampleApp2 = activities.get(1);
+ mSampleApp3 = activities.get(2);
+
+ // Disable app tracker
+ AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
+
+ mCallback = PredictionUiStateManager.INSTANCE.get(mTargetContext).appPredictorCallback(
+ Client.HOME);
+
+ mDevice.setOrientationNatural();
+ }
+
+ @After
+ public void tearDown() throws Throwable {
+ mDevice.unfreezeRotation();
+ }
+
+ /**
+ * Test that prediction UI is updated as soon as we get predictions from the system
+ */
+ @Test
+ public void testPredictionExistsInAllApps() {
+ mActivityMonitor.startLauncher();
+ mLauncher.pressHome().switchToAllApps();
+
+ // There has not been any update, verify that progress bar is showing
+ waitForLauncherCondition("Prediction is not in loading state", launcher -> {
+ ProgressBar p = findLoadingBar(launcher);
+ return p != null && p.isShown();
+ });
+
+ // Dispatch an update
+ sendPredictionUpdate(mSampleApp1, mSampleApp2);
+ waitForLauncherCondition("Predictions were not updated in loading state",
+ launcher -> getPredictedApp(launcher).size() == 2);
+ }
+
+ /**
+ * Test tat prediction update is deferred if it is already visible
+ */
+ @Test
+ public void testPredictionsDeferredUntilHome() {
+ mActivityMonitor.startLauncher();
+ sendPredictionUpdate(mSampleApp1, mSampleApp2);
+ mLauncher.pressHome().switchToAllApps();
+ waitForLauncherCondition("Predictions were not updated in loading state",
+ launcher -> getPredictedApp(launcher).size() == 2);
+
+ // Update predictions while all-apps is visible
+ sendPredictionUpdate(mSampleApp1, mSampleApp2, mSampleApp3);
+ assertEquals(2, getFromLauncher(this::getPredictedApp).size());
+
+ // Go home and go back to all-apps
+ mLauncher.pressHome().switchToAllApps();
+ assertEquals(3, getFromLauncher(this::getPredictedApp).size());
+ }
+
+ public ArrayList getPredictedApp(Launcher launcher) {
+ PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
+ .findFixedRowByType(PredictionRowView.class);
+
+ ArrayList predictedAppViews = new ArrayList<>();
+ for (int i = 0; i < container.getChildCount(); i++) {
+ View view = container.getChildAt(i);
+ if (view instanceof BubbleTextView && view.getVisibility() == View.VISIBLE) {
+ predictedAppViews.add((BubbleTextView) view);
+ }
+ }
+ return predictedAppViews;
+ }
+
+ private ProgressBar findLoadingBar(Launcher launcher) {
+ PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
+ .findFixedRowByType(PredictionRowView.class);
+
+ for (int i = 0; i < container.getChildCount(); i++) {
+ View view = container.getChildAt(i);
+ if (view instanceof ProgressBar) {
+ return (ProgressBar) view;
+ }
+ }
+ return null;
+ }
+
+
+ private void sendPredictionUpdate(LauncherActivityInfo... activities) {
+ getOnUiThread(() -> {
+ List targets = new ArrayList<>(activities.length);
+ for (LauncherActivityInfo info : activities) {
+ ComponentName cn = info.getComponentName();
+ AppTarget target = new AppTarget.Builder(new AppTargetId("app:" + cn))
+ .setTarget(cn.getPackageName(), info.getUser())
+ .setClassName(cn.getClassName())
+ .build();
+ targets.add(target);
+ }
+ mCallback.onTargetsAvailable(targets);
+ return null;
+ });
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
new file mode 100644
index 0000000000..87981571bd
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -0,0 +1,87 @@
+package com.android.quickstep;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.LauncherState.OVERVIEW;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.PendingIntent;
+import android.app.usage.UsageStatsManager;
+import android.content.Intent;
+
+import com.android.launcher3.Launcher;
+import com.android.quickstep.views.DigitalWellBeingToast;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+import org.junit.Test;
+
+import java.time.Duration;
+
+public class DigitalWellBeingToastTest extends AbstractQuickStepTest {
+ private static final String CALCULATOR_PACKAGE =
+ resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
+
+ @Test
+ public void testToast() throws Exception {
+ final UsageStatsManager usageStatsManager =
+ mTargetContext.getSystemService(UsageStatsManager.class);
+ final int observerId = 0;
+
+ try {
+ final String[] packages = new String[]{CALCULATOR_PACKAGE};
+
+ // Set time limit for app.
+ runWithShellPermission(() ->
+ usageStatsManager.registerAppUsageLimitObserver(observerId, packages,
+ Duration.ofSeconds(600), Duration.ofSeconds(300),
+ PendingIntent.getActivity(mTargetContext, -1, new Intent(), 0)));
+
+ mLauncher.pressHome();
+ final DigitalWellBeingToast toast = getToast();
+
+ assertTrue("Toast is not visible", toast.isShown());
+ assertEquals("Toast text: ", "5 minutes left today", toast.getTextView().getText());
+
+ // Unset time limit for app.
+ runWithShellPermission(
+ () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
+
+ mLauncher.pressHome();
+ assertFalse("Toast is visible", getToast().isShown());
+ } finally {
+ runWithShellPermission(
+ () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
+ }
+ }
+
+ private DigitalWellBeingToast getToast() {
+ executeOnLauncher(launcher -> launcher.getStateManager().goToState(OVERVIEW));
+ waitForState("Launcher internal state didn't switch to Overview", OVERVIEW);
+ waitForLauncherCondition("No latest task", launcher -> getLatestTask(launcher) != null);
+
+ return getFromLauncher(launcher -> {
+ final TaskView task = getLatestTask(launcher);
+ assertTrue("Latest task is not Calculator",
+ CALCULATOR_PACKAGE.equals(task.getTask().getTopComponent().getPackageName()));
+ return task.getDigitalWellBeingToast();
+ });
+ }
+
+ private TaskView getLatestTask(Launcher launcher) {
+ return launcher.getOverviewPanel().getTaskViewAt(0);
+ }
+
+ private void runWithShellPermission(Runnable action) {
+ getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+ try {
+ action.run();
+ } finally {
+ getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 6034791d71..e552f56ba6 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -21,6 +21,7 @@ import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.android.quickstep.NavigationModeSwitchRule.Mode.ALL;
import static com.android.quickstep.NavigationModeSwitchRule.Mode.THREE_BUTTON;
import static com.android.quickstep.NavigationModeSwitchRule.Mode.TWO_BUTTON;
+import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_2BUTTON_OVERLAY;
import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_3BUTTON_OVERLAY;
import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_GESTURAL_OVERLAY;
@@ -85,9 +86,9 @@ public class NavigationModeSwitchRule implements TestRule {
final LauncherInstrumentation.NavigationModel originalMode =
mLauncher.getNavigationModel();
try {
-// if (mode == ZERO_BUTTON || mode == ALL) {
-// evaluateWithZeroButtons();
-// }
+ if (mode == ZERO_BUTTON || mode == ALL) {
+ evaluateWithZeroButtons();
+ }
if (mode == TWO_BUTTON || mode == ALL) {
evaluateWithTwoButtons();
}
@@ -131,7 +132,10 @@ public class NavigationModeSwitchRule implements TestRule {
overlayPackage == NAV_BAR_MODE_GESTURAL_OVERLAY);
for (int i = 0; i != 100; ++i) {
- if (mLauncher.getNavigationModel() == expectedMode) return;
+ if (mLauncher.getNavigationModel() == expectedMode) {
+ Thread.sleep(1000);
+ return;
+ }
Thread.sleep(100);
}
Assert.fail("Couldn't switch to " + overlayPackage);
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index dc83e87639..a47c8e7a99 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -61,7 +61,7 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest {
}
@Test
- @Ignore // Ignoring until gestural navigation event sequence settles
+ @Ignore // Ignoring until race condition repro framework is changes for multi-process case.
@NavigationModeSwitch(mode = Mode.TWO_BUTTON)
public void testPressHome() {
runTest(enterEvt(Launcher.ON_CREATE_EVT),
@@ -75,14 +75,6 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest {
exitEvt(Launcher.ON_CREATE_EVT));
}
- @Test
- @Ignore // Ignoring until gestural navigation event sequence settles
- @NavigationModeSwitch(mode = Mode.TWO_BUTTON)
- public void testSwipeToOverview() {
- closeLauncherActivity();
- mLauncher.getBackground().switchToOverview();
- }
-
@Test
@NavigationModeSwitch
public void testStressPressHome() {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 1709516fc7..b0ce5f5dc9 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -16,6 +16,8 @@
package com.android.quickstep;
+import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -60,7 +62,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
}
private void startTestApps() throws Exception {
- startAppFast(resolveSystemApp(Intent.CATEGORY_APP_BROWSER));
+ startAppFast(getAppPackageName());
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CONTACTS));
@@ -72,7 +74,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
@Test
@PortraitLandscape
- @Ignore
+ @Ignore // Enable after b/131115533
public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException {
mDevice.pressRecentApps();
waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW);
diff --git a/res/drawable/ic_close.xml b/res/drawable/ic_close.xml
deleted file mode 100644
index 8b2f55fb32..0000000000
--- a/res/drawable/ic_close.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/res/drawable/ic_corp.xml b/res/drawable/ic_corp.xml
index 48f50076c9..b59113dc6c 100644
--- a/res/drawable/ic_corp.xml
+++ b/res/drawable/ic_corp.xml
@@ -17,8 +17,11 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
- android:viewportHeight="24.0">
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/textColorHint" >
+ android:pathData="M20 6h-4V4c0-1.11-0.89-2-2-2h-4c-1.11 0-2 0.89-2 2v2H4c-1.11 0-1.99 0.89 -1.99
+2L2 19c0 1.11 0.89 2 2 2h16c1.11 0 2-0.89 2-2V8c0-1.11-0.89-2-2-2zM10
+4h4v2h-4V4zm10 15H4V8h16v11z"
+ android:fillColor="@android:color/white"/>
\ No newline at end of file
diff --git a/res/drawable/deep_shortcuts_drag_handle.xml b/res/drawable/ic_drag_handle.xml
similarity index 81%
rename from res/drawable/deep_shortcuts_drag_handle.xml
rename to res/drawable/ic_drag_handle.xml
index 8fc3779f60..0181ff15d7 100644
--- a/res/drawable/deep_shortcuts_drag_handle.xml
+++ b/res/drawable/ic_drag_handle.xml
@@ -22,7 +22,6 @@
android:tint="?android:attr/textColorHint" >
\ No newline at end of file
diff --git a/res/drawable/ic_info_no_shadow.xml b/res/drawable/ic_info_no_shadow.xml
index d816f12b7e..7c43779efa 100644
--- a/res/drawable/ic_info_no_shadow.xml
+++ b/res/drawable/ic_info_no_shadow.xml
@@ -21,12 +21,12 @@
android:tint="?android:attr/textColorPrimary">
diff --git a/res/drawable/ic_install_no_shadow.xml b/res/drawable/ic_install_no_shadow.xml
index ffce22aac1..eaad0de604 100644
--- a/res/drawable/ic_install_no_shadow.xml
+++ b/res/drawable/ic_install_no_shadow.xml
@@ -17,11 +17,13 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
- android:viewportHeight="24">
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorPrimary">
+ android:fillColor="@android:color/white"
+ android:pathData="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z" />
+ android:fillColor="@android:color/white"
+ android:pathData="M 17 11.5 L 15.59 10.09 L 13 12.67 L 13 4 L 11 4 L 11 12.67 L 8.41 10.09 L 7 11.5 L 12 16.5 Z" />
diff --git a/res/drawable/ic_palette.xml b/res/drawable/ic_palette.xml
new file mode 100644
index 0000000000..21cec2d37c
--- /dev/null
+++ b/res/drawable/ic_palette.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/ic_remove_no_shadow.xml b/res/drawable/ic_remove_no_shadow.xml
index be7f9f354b..2c706dbb30 100644
--- a/res/drawable/ic_remove_no_shadow.xml
+++ b/res/drawable/ic_remove_no_shadow.xml
@@ -14,14 +14,12 @@
limitations under the License.
-->
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0"
+ android:tint="?android:attr/textColorPrimary">
+ android:fillColor="@android:color/white"
+ android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41z" />
diff --git a/res/drawable/ic_setting.xml b/res/drawable/ic_setting.xml
index a83aab3204..8a3728cfa2 100644
--- a/res/drawable/ic_setting.xml
+++ b/res/drawable/ic_setting.xml
@@ -20,7 +20,7 @@ Copyright (C) 2016 The Android Open Source Project
android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary" >
diff --git a/res/drawable/ic_uninstall_no_shadow.xml b/res/drawable/ic_uninstall_no_shadow.xml
index 37632d1a4b..6aff102a2d 100644
--- a/res/drawable/ic_uninstall_no_shadow.xml
+++ b/res/drawable/ic_uninstall_no_shadow.xml
@@ -20,12 +20,12 @@
android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary" >
diff --git a/res/drawable/ic_wallpaper.xml b/res/drawable/ic_wallpaper.xml
index 7fd93403c5..9543f88192 100644
--- a/res/drawable/ic_wallpaper.xml
+++ b/res/drawable/ic_wallpaper.xml
@@ -1,5 +1,5 @@
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index c77b0b931d..64f2362ed3 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -34,7 +34,6 @@
android:layout_weight="1"
android:ellipsize="end"
android:fadingEdge="horizontal"
- android:fontFamily="sans-serif-condensed"
android:gravity="start"
android:singleLine="true"
android:maxLines="1"
@@ -51,7 +50,6 @@
android:layout_marginLeft="5dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
- android:fontFamily="sans-serif-condensed"
android:alpha="0.8" />
diff --git a/res/layout/widgets_bottom_sheet.xml b/res/layout/widgets_bottom_sheet.xml
index 6bf90481ad..3fdfc96667 100644
--- a/res/layout/widgets_bottom_sheet.xml
+++ b/res/layout/widgets_bottom_sheet.xml
@@ -26,7 +26,7 @@
android:theme="?attr/widgetsTheme">
+ android:src="@drawable/ic_remove_no_shadow"/>
-
+
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ec63e35974..9b84cc9a34 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -75,8 +75,8 @@
- #EA212121
- 102
- #80000000
- - ?android:attr/colorPrimary
- - #424242
+ - #3C4043
+ - #5F6368
- #757575
- @style/WidgetContainerTheme.Dark
- #FF464646
@@ -126,7 +126,7 @@
-
-