Merging ub-launcher3-qt-dev, build 5505393

Test: Manual
Bug:111411630 Dark theme popup blends in with the normal background
Bug:113952590 [STOP SHIP BUG] WM Experiment: Slim the navigation bar
Bug:114136250 Have a more spartan RecentsActivity on android go
Bug:117106893 Enable all @Ignore-d Launcher tests
Bug:118678948 A11Y: TalkBack: After adding shortcut app icon on the homescreen, sometimes shortcuts app icons are not accessible with TalkBack.
Bug:122700646 Launcher crashed after uninstalling app
Bug:123985787 Respect dialogCornerRadius for all apps / widgets / settings / shortcut menu / dwb / task / overview (everything)
Bug:126229665 Launcher home screen should properly use Headline/ body font
Bug:126450861 Tab structure update and tips for Theme Picker
Bug:126587956 Create an app to use with Launcher testing
Bug:126596502 [touchflow] Use platform long press in launcher
Bug:126738409 Icon packs for launcher
Bug:126822309 Launcher > Overview > "No recent item" is not using theme font
Bug:127689526 [icons] hourglass_bottom and hourglass_top
Bug:127840207 [logging] quickswitch usage
Bug:128531133 Make TAPL diagnostics clearer
Bug:128649657 Define overlayable.xml for icon packs
Bug:128808922 [QT-QP1A] Click to change orientation in GCA secure mode will lead to unlock screen.
Bug:128857973 With dark display and black accent device theme, App list header not visible on all apps screen.
Bug:129145824 When predictions are disabled, the hotseat does not appear in quickstep
Bug:129146717 Do not allow overlays to change attribute format
Bug:129297464 [Gesture Nav] Exclude edges from most Launcher / Overview states.
Bug:129328259 [Regression] Suspended app icons turn full colour during open animation
Bug:129421279 FloatingIconView should be removed when user interacts with launcher
Bug:129434166 Lab-only flake: drag to workspace doesn't happen
Bug:129785904 [Gesture Nav] Verify that gestures are disabled appropriately in transient states
Bug:129955150 Launcher settings' icon is not using consistent grey
Bug:129985827 [Fully Gestural Navigation] Delay Recents animation when swiping up
Bug:130053407 [Tests broken] testLearn: settings is not present in the prediction row
Bug:130160876 Full gesture navigation with fast switcher shows part of "clear all" message
Bug:130415177 Launcher displays app as suspended when it is not if setPackagesSuspended is called multiple times
Bug:130440957 Recents Go view visibility not updating while loading UI is up
Bug:130442915 Jank observed while navigating from long press app shortcut.
Bug:130562632 DefaultLayoutProviderTest.testCustomProfileLoaded_with_folder is flaking
Bug:130580680 Recents Go does not load views right after boot
Bug:130635650 Recents Go thumbnails not updating on app => recents transition
Bug:130735711 Should fix overview shortcut in Recents Go landscape mode
Bug:130739123 Tests failure: Attempt to invoke virtual method 'int com.android.launcher3.views.BaseDragLayer.getChildCount()' on a null object reference
Bug:130740246 Recents Go crashes from ViewTreeObserver being dead
Bug:130746661 Fix NPE for getting task from loading task UI in Recents Go
Bug:130820737 Recents Go remove animation doesn't always work
Bug:130914022 Add a test for DWB toast
Bug:130917995 onConfigChanged when apps are auto installed
Bug:131095241 Recents Go should support landscape app => landscape thumbnail transition
Bug:131102753 crosshatch_svelte doesn't boot on ToT master on 4/22 because of FATAL exception in launcher3
Bug:131170582 [Flaky test] TaplTestsLauncher3.testDragShortcut
Bug:131250710 ControlType.CLEAL_ALL_BUTTON logging is gone
Bug:78240090 (Quickstep) Swiping down on the shelf while scrolling carousel
Change-Id: I4ef9beb24fab6be4fcff44b330e30d486fc01f03
This commit is contained in:
Winson Chung
2019-04-25 10:02:23 -07:00
219 changed files with 5285 additions and 1579 deletions
+3 -2
View File
@@ -251,7 +251,7 @@ LOCAL_PROGUARD_ENABLED := full
LOCAL_PACKAGE_NAME := Launcher3QuickStepGo LOCAL_PACKAGE_NAME := Launcher3QuickStepGo
LOCAL_PRIVILEGED_MODULE := true LOCAL_PRIVILEGED_MODULE := true
LOCAL_PRODUCT_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_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
LOCAL_FULL_LIBS_MANIFEST_FILES := \ LOCAL_FULL_LIBS_MANIFEST_FILES := \
@@ -297,7 +297,8 @@ LOCAL_PROGUARD_ENABLED := full
LOCAL_PACKAGE_NAME := Launcher3GoIconRecents LOCAL_PACKAGE_NAME := Launcher3GoIconRecents
LOCAL_PRIVILEGED_MODULE := true 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_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
LOCAL_FULL_LIBS_MANIFEST_FILES := \ LOCAL_FULL_LIBS_MANIFEST_FILES := \
@@ -14,4 +14,9 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<merge/> <shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/darker_gray"/>
<corners android:radius="2dp"/>
</shape>
@@ -14,10 +14,10 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<com.android.quickstep.hints.ChipsContainer <shape
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:shape="rectangle">
android:layout_height="@dimen/chip_hint_height" <solid android:color="@android:color/transparent"/>
android:layout_gravity="bottom" <stroke android:color="@android:color/white" android:width="4px"/>
android:gravity="center_horizontal" <corners android:radius="2dp"/>
android:orientation="horizontal"/> </shape>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<com.android.quickstep.views.ClearAllItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/clear_all_item_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/clear_all_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="16dp"
android:layout_gravity="center_horizontal"
android:paddingHorizontal="32dp"
android:background="@drawable/clear_all_button"
android:gravity="center"
android:text="@string/recents_clear_all"
android:textAllCaps="false"
android:textColor="@color/clear_all_button_text"
android:textSize="14sp"/>
</com.android.quickstep.views.ClearAllItemView>
@@ -19,31 +19,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recent_task_content_view" android:id="@+id/recent_task_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:scrollbars="none"/>
android:visibility="gone">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recent_task_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:scrollbars="none"/>
<Button
android:id="@+id/clear_all_button"
android:layout_width="@dimen/clear_all_button_width"
android:layout_height="@dimen/clear_all_button_height"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="@dimen/task_item_half_vert_margin"
android:background="@drawable/clear_all_button"
android:gravity="center"
android:text="@string/recents_clear_all"
android:textAllCaps="false"
android:textColor="@color/clear_all_button_text"
android:textSize="14sp"/>
</LinearLayout>
<TextView <TextView
android:id="@+id/recent_task_empty_view" android:id="@+id/recent_task_empty_view"
android:layout_width="match_parent" android:layout_width="match_parent"
+10 -13
View File
@@ -17,26 +17,23 @@
<com.android.quickstep.views.TaskItemView <com.android.quickstep.views.TaskItemView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="horizontal"> android:orientation="horizontal">
<FrameLayout <com.android.quickstep.views.TaskThumbnailIconView
android:id="@+id/task_icon_and_thumbnail" android:id="@+id/task_icon_and_thumbnail"
android:layout_width="@dimen/task_thumbnail_and_icon_view_size" android:layout_width="match_parent"
android:layout_height="@dimen/task_thumbnail_and_icon_view_size" android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="8dp" android:layout_marginHorizontal="8dp"
android:layout_marginVertical="@dimen/task_item_half_vert_margin"> android:layout_marginTop="16dp">
<ImageView <ImageView
android:id="@+id/task_thumbnail" android:id="@+id/task_thumbnail"
android:layout_width="@dimen/task_thumbnail_width" android:layout_width="match_parent"
android:layout_height="@dimen/task_thumbnail_height" android:layout_height="match_parent"/>
android:layout_gravity="top|start"/>
<ImageView <ImageView
android:id="@+id/task_icon" android:id="@+id/task_icon"
android:layout_width="@dimen/task_icon_size" android:layout_width="match_parent"
android:layout_height="@dimen/task_icon_size" android:layout_height="match_parent"/>
android:layout_gravity="bottom|end"/> </com.android.quickstep.views.TaskThumbnailIconView>
</FrameLayout>
<TextView <TextView
android:id="@+id/task_label" android:id="@+id/task_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<dimen name="task_item_half_vert_margin">8dp</dimen>
<dimen name="task_thumbnail_and_icon_view_size">60dp</dimen>
<dimen name="task_thumbnail_height">60dp</dimen>
<dimen name="task_thumbnail_width">36dp</dimen>
<dimen name="task_icon_size">36dp</dimen>
<dimen name="clear_all_button_width">106dp</dimen>
<dimen name="clear_all_button_height">36dp</dimen>
</resources>
@@ -18,6 +18,7 @@ package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
import static com.android.launcher3.anim.Interpolators.DEACCEL_2; 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.DeviceProfile;
import com.android.launcher3.Launcher; import com.android.launcher3.Launcher;
@@ -51,6 +52,9 @@ public class OverviewState extends LauncherState {
public void onStateEnabled(Launcher launcher) { public void onStateEnabled(Launcher launcher) {
IconRecentsView recentsView = launcher.getOverviewPanel(); IconRecentsView recentsView = launcher.getOverviewPanel();
recentsView.onBeginTransitionToOverview(); recentsView.onBeginTransitionToOverview();
// Request orientation be set to unspecified, letting the system decide the best
// orientation.
launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
} }
@Override @Override
@@ -131,10 +131,11 @@ final class AppToOverviewAnimationProvider<T extends BaseDraggingActivity> imple
return anim; return anim;
} }
View thumbnailView = mRecentsView.getThumbnailViewForTask(mTargetTaskId); View thumbnailView = mRecentsView.getBottomThumbnailView();
if (thumbnailView == null) { if (thumbnailView == null) {
// TODO: We should either 1) guarantee the view is loaded before attempting this // This can be null if there were previously 0 tasks and the recycler view has not had
// or 2) have a backup animation. // 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)) { if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "No thumbnail view for running task. Using stub animation."); Log.w(TAG, "No thumbnail view for running task. Using stub animation.");
} }
@@ -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);
}
}
@@ -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<PendingAnimation> mPendingAnims = new ArrayList<>();
/**
* Animations that have started and are running.
*/
private final ArrayList<ObjectAnimator> 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<Object> 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();
}
}
}
}
@@ -15,9 +15,13 @@
*/ */
package com.android.quickstep; package com.android.quickstep;
import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
import android.app.ActivityOptions; import android.app.ActivityOptions;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import com.android.quickstep.views.TaskItemView; import com.android.quickstep.views.TaskItemView;
import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper; 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 * @param viewHolder the task view holder to launch
*/ */
public void launchTask(TaskHolder viewHolder) { public void launchTaskFromView(@NonNull TaskHolder viewHolder) {
if (viewHolder.getTask() == null) { if (!viewHolder.getTask().isPresent()) {
return; return;
} }
TaskItemView itemView = (TaskItemView) (viewHolder.itemView); TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
@@ -53,8 +57,20 @@ public final class TaskActionController {
int height = v.getMeasuredHeight(); int height = v.getMeasuredHeight();
ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height); ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(viewHolder.getTask().key, ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(
opts, null /* resultCallback */, null /* resultCallbackHandler */); 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 * @param viewHolder the task view holder to remove
*/ */
public void removeTask(TaskHolder viewHolder) { public void removeTask(TaskHolder viewHolder) {
if (viewHolder.getTask() == null) { if (!viewHolder.getTask().isPresent()) {
return; return;
} }
int position = viewHolder.getAdapterPosition(); int position = viewHolder.getAdapterPosition();
Task task = viewHolder.getTask(); Task task = viewHolder.getTask().get();
ActivityManagerWrapper.getInstance().removeTask(task.key.id); ActivityManagerWrapper.getInstance().removeTask(task.key.id);
mLoader.removeTask(task); mLoader.removeTask(task);
mAdapter.notifyItemRemoved(position); mAdapter.notifyItemRemoved(position);
@@ -80,6 +96,6 @@ public final class TaskActionController {
int count = mAdapter.getItemCount(); int count = mAdapter.getItemCount();
ActivityManagerWrapper.getInstance().removeAllRecentTasks(); ActivityManagerWrapper.getInstance().removeAllRecentTasks();
mLoader.clearAllTasks(); mLoader.clearAllTasks();
mAdapter.notifyItemRangeRemoved(0 /* positionStart */, count); mAdapter.notifyItemRangeRemoved(TASKS_START_POSITION /* positionStart */, count);
} }
} }
@@ -15,13 +15,15 @@
*/ */
package com.android.quickstep; package com.android.quickstep;
import android.util.ArrayMap;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView.Adapter; import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.launcher3.R; import com.android.launcher3.R;
import com.android.quickstep.views.TaskItemView; 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.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
/** /**
* Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the * Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the
* appropriate {@link Task} from the recents task list. * appropriate {@link Task} from the recents task list.
*/ */
public final class TaskAdapter extends Adapter<TaskHolder> { public final class TaskAdapter extends Adapter<ViewHolder> {
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 static final String TAG = "TaskAdapter";
private final TaskListLoader mLoader; private final TaskListLoader mLoader;
private final ArrayMap<Integer, TaskItemView> mTaskIdToViewMap = new ArrayMap<>();
private TaskActionController mTaskActionController; private TaskActionController mTaskActionController;
private OnClickListener mClearAllListener;
private boolean mIsShowingLoadingUi; private boolean mIsShowingLoadingUi;
public TaskAdapter(@NonNull TaskListLoader loader) { public TaskAdapter(@NonNull TaskListLoader loader) {
@@ -51,6 +60,10 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
mTaskActionController = taskActionController; mTaskActionController = taskActionController;
} }
public void setOnClearAllClickListener(OnClickListener listener) {
mClearAllListener = listener;
}
/** /**
* Sets all positions in the task adapter to loading views, binding new views if necessary. * 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 * 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<TaskHolder> {
mIsShowingLoadingUi = isShowingLoadingUi; mIsShowingLoadingUi = isShowingLoadingUi;
} }
/** @Override
* Get task item view for a given task id if it's attached to the view. public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
* switch (viewType) {
* @param taskId task id to search for case ITEM_TYPE_TASK:
* @return corresponding task item view if it's attached, null otherwise TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
*/ .inflate(R.layout.task_item_view, parent, false);
public @Nullable TaskItemView getTaskItemView(int taskId) { TaskHolder taskHolder = new TaskHolder(itemView);
return mTaskIdToViewMap.get(taskId); 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 @Override
public TaskHolder onCreateViewHolder(ViewGroup parent, int viewType) { public void onBindViewHolder(ViewHolder holder, int position) {
TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext()) onBindViewHolderInternal(holder, position, false /* willAnimate */);
.inflate(R.layout.task_item_view, parent, false);
TaskHolder holder = new TaskHolder(itemView);
itemView.setOnClickListener(view -> mTaskActionController.launchTask(holder));
return holder;
} }
@Override @Override
public void onBindViewHolder(TaskHolder holder, int position) { public void onBindViewHolder(@NonNull ViewHolder holder, int position,
if (mIsShowingLoadingUi) { @NonNull List<Object> payloads) {
holder.bindEmptyUi(); if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
return; return;
} }
List<Task> tasks = mLoader.getCurrentTaskList(); int changeType = (int) payloads.get(0);
if (position >= tasks.size()) { if (changeType == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
// Task list has updated. // Bind in preparation for animation
return; 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<Task> 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 @Override
public void onViewAttachedToWindow(@NonNull TaskHolder holder) { public int getItemViewType(int position) {
if (holder.getTask() == null) { // Bottom is always clear all button.
return; return (position == 0) ? ITEM_TYPE_CLEAR_ALL : ITEM_TYPE_TASK;
}
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);
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
int itemCount = TASKS_START_POSITION;
if (mIsShowingLoadingUi) { if (mIsShowingLoadingUi) {
// Show loading version of all items. // Show loading version of all items.
return MAX_TASKS_TO_DISPLAY; itemCount += MAX_TASKS_TO_DISPLAY;
} else { } else {
return Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY); itemCount += Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
} }
return itemCount;
} }
} }
@@ -15,12 +15,14 @@
*/ */
package com.android.quickstep; package com.android.quickstep;
import androidx.annotation.Nullable; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder; import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.quickstep.views.TaskItemView; import com.android.quickstep.views.TaskItemView;
import com.android.systemui.shared.recents.model.Task; 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, * A recycler view holder that holds the task view and binds {@link Task} content (app title, icon,
* etc.) to the view. * 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 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; 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() { public void bindEmptyUi() {
mTask = null; mTask = null;
// TODO: Set the task view to a loading, empty UI. mTaskItemView.resetToEmptyUi();
// Temporarily using the one below for visual confirmation but should be swapped out to new
// UI later.
mTaskItemView.resetTaskItemView();
} }
/** /**
@@ -66,7 +79,7 @@ public final class TaskHolder extends ViewHolder {
* *
* @return the current task * @return the current task
*/ */
public @Nullable Task getTask() { public Optional<Task> getTask() {
return mTask; return Optional.ofNullable(mTask);
} }
} }
@@ -38,23 +38,9 @@ public final class TaskListLoader {
private ArrayList<Task> mTaskList = new ArrayList<>(); private ArrayList<Task> mTaskList = new ArrayList<>();
private int mTaskListChangeId; 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) { public TaskListLoader(Context context) {
mRecentsModel = RecentsModel.INSTANCE.get(context); mRecentsModel = RecentsModel.INSTANCE.get(context);
mRecentsModel.addThumbnailChangeListener(listener);
} }
/** /**
@@ -17,6 +17,9 @@ package com.android.quickstep;
import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT; 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.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder; import androidx.recyclerview.widget.RecyclerView.ViewHolder;
@@ -45,4 +48,14 @@ public final class TaskSwipeCallback extends ItemTouchHelper.SimpleCallback {
mTaskActionController.removeTask((TaskHolder) viewHolder); 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);
}
} }
@@ -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);
}
}
@@ -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);
}
}
@@ -15,8 +15,13 @@
*/ */
package com.android.quickstep.views; package com.android.quickstep.views;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL; 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.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
@@ -24,30 +29,39 @@ import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder; import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.content.Context; import android.content.Context;
import android.util.ArraySet;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.FloatProperty; import android.util.FloatProperty;
import android.view.View; import android.view.View;
import android.view.ViewDebug; import android.view.ViewDebug;
import android.view.animation.AlphaAnimation; import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.LayoutAnimationController;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; 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.launcher3.R;
import com.android.quickstep.ContentFillItemAnimator;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.RecentsToActivityHelper; import com.android.quickstep.RecentsToActivityHelper;
import com.android.quickstep.TaskActionController; import com.android.quickstep.TaskActionController;
import com.android.quickstep.TaskAdapter; import com.android.quickstep.TaskAdapter;
import com.android.quickstep.TaskHolder; import com.android.quickstep.TaskHolder;
import com.android.quickstep.TaskListLoader; import com.android.quickstep.TaskListLoader;
import com.android.quickstep.TaskSwipeCallback; 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 * 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 Context mContext;
private final TaskListLoader mTaskLoader; private final TaskListLoader mTaskLoader;
private final TaskAdapter mTaskAdapter; private final TaskAdapter mTaskAdapter;
private final LinearLayoutManager mTaskLayoutManager;
private final TaskActionController mTaskActionController; private final TaskActionController mTaskActionController;
private final LayoutAnimationController mLayoutAnimation; private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
private final ContentFillItemAnimator mLoadingContentItemAnimator =
new ContentFillItemAnimator();
private RecentsToActivityHelper mActivityHelper; private RecentsToActivityHelper mActivityHelper;
private RecyclerView mTaskRecyclerView; private RecyclerView mTaskRecyclerView;
private View mShowingContentView;
private View mEmptyView; private View mEmptyView;
private View mContentView; private View mContentView;
private View mClearAllView;
private boolean mTransitionedFromApp; private boolean mTransitionedFromApp;
private AnimatorSet mLayoutAnimation;
private final ArraySet<View> mLayingOutViews = new ArraySet<>();
private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
ArrayList<TaskItemView> itemViews = getTaskViews();
for (int i = 0, size = itemViews.size(); i < size; i++) {
TaskItemView taskView = itemViews.get(i);
TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
Optional<Task> 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) { public IconRecentsView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
BaseActivity activity = BaseActivity.fromContext(context);
mContext = context; mContext = context;
mTaskLoader = new TaskListLoader(mContext); mTaskLoader = new TaskListLoader(mContext);
mTaskAdapter = new TaskAdapter(mTaskLoader); mTaskAdapter = new TaskAdapter(mTaskLoader);
mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter); mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
mTaskAdapter.setActionController(mTaskActionController); mTaskAdapter.setActionController(mTaskActionController);
mLayoutAnimation = createLayoutAnimation(); mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
} }
@Override @Override
@@ -115,15 +153,30 @@ public final class IconRecentsView extends FrameLayout {
if (mTaskRecyclerView == null) { if (mTaskRecyclerView == null) {
mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view); mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
mTaskRecyclerView.setAdapter(mTaskAdapter); mTaskRecyclerView.setAdapter(mTaskAdapter);
mTaskRecyclerView.setLayoutManager( mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */));
ItemTouchHelper helper = new ItemTouchHelper( ItemTouchHelper helper = new ItemTouchHelper(
new TaskSwipeCallback(mTaskActionController)); new TaskSwipeCallback(mTaskActionController));
helper.attachToRecyclerView(mTaskRecyclerView); 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); mEmptyView = findViewById(R.id.recent_task_empty_view);
mContentView = findViewById(R.id.recent_task_content_view); mContentView = mTaskRecyclerView;
mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() { mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override @Override
public void onChanged() { public void onChanged() {
@@ -135,19 +188,17 @@ public final class IconRecentsView extends FrameLayout {
updateContentViewVisibility(); updateContentViewVisibility();
} }
}); });
mClearAllView = findViewById(R.id.clear_all_button); // TODO: Move layout param logic into onMeasure
mClearAllView.setOnClickListener(v -> animateClearAllTasks());
} }
} }
@Override @Override
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
super.setEnabled(enabled); super.setEnabled(enabled);
TaskItemView[] itemViews = getTaskViews(); int childCount = mTaskRecyclerView.getChildCount();
for (TaskItemView itemView : itemViews) { for (int i = 0; i < childCount; i++) {
itemView.setEnabled(enabled); mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
} }
mClearAllView.setEnabled(enabled);
} }
/** /**
@@ -165,8 +216,13 @@ public final class IconRecentsView extends FrameLayout {
* becomes visible. * becomes visible.
*/ */
public void onBeginTransitionToOverview() { 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 // Load any task changes
if (!mTaskLoader.needsToLoad()) { if (!mTaskLoader.needsToLoad()) {
return; return;
@@ -174,9 +230,24 @@ public final class IconRecentsView extends FrameLayout {
mTaskAdapter.setIsShowingLoadingUi(true); mTaskAdapter.setIsShowingLoadingUi(true);
mTaskAdapter.notifyDataSetChanged(); mTaskAdapter.notifyDataSetChanged();
mTaskLoader.loadTaskList(tasks -> { mTaskLoader.loadTaskList(tasks -> {
int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
mTaskAdapter.setIsShowingLoadingUi(false); mTaskAdapter.setIsShowingLoadingUi(false);
// TODO: Animate the loading UI out and the loaded data in. int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
mTaskAdapter.notifyDataSetChanged(); 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. * the app. In that case, we launch the next most recent.
*/ */
public void handleOverviewCommand() { public void handleOverviewCommand() {
int childCount = mTaskRecyclerView.getChildCount(); List<Task> tasks = mTaskLoader.getCurrentTaskList();
if (childCount == 0) { int tasksSize = tasks.size();
if (tasksSize == 0) {
// Do nothing // Do nothing
return; return;
} }
TaskHolder taskToLaunch; Task taskToLaunch;
if (mTransitionedFromApp && childCount > 1) { if (mTransitionedFromApp && tasksSize > 1) {
// Launch the next most recent app // Launch the next most recent app
TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(1); taskToLaunch = tasks.get(1);
taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
} else { } else {
// Launch the most recent app // Launch the most recent app
TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(0); taskToLaunch = tasks.get(0);
taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
} }
// See if view for this task is attached, and if so, animate launch from that view.
ArrayList<TaskItemView> 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); 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 if laid out
* @return the thumbnail view for the task if attached, null otherwise
*/ */
public @Nullable View getThumbnailViewForTask(int taskId) { public @Nullable View getBottomThumbnailView() {
TaskItemView view = mTaskAdapter.getTaskItemView(taskId); ArrayList<TaskItemView> taskViews = getTaskViews();
if (view == null) { if (taskViews.isEmpty()) {
return null; return null;
} }
TaskItemView view = taskViews.get(0);
return view.getThumbnailView(); return view.getThumbnailView();
} }
@@ -231,13 +314,14 @@ public final class IconRecentsView extends FrameLayout {
*/ */
private void animateClearAllTasks() { private void animateClearAllTasks() {
setEnabled(false); setEnabled(false);
TaskItemView[] itemViews = getTaskViews(); ArrayList<TaskItemView> itemViews = getTaskViews();
AnimatorSet clearAnim = new AnimatorSet(); AnimatorSet clearAnim = new AnimatorSet();
long currentDelay = 0; long currentDelay = 0;
// Animate each item view to the right and fade out. // 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, PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO); 0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f); PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
@@ -272,7 +356,8 @@ public final class IconRecentsView extends FrameLayout {
clearAnim.addListener(new AnimatorListenerAdapter() { clearAnim.addListener(new AnimatorListenerAdapter() {
@Override @Override
public void onAnimationEnd(Animator animation) { 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.setTranslationX(0);
itemView.setAlpha(1.0f); itemView.setAlpha(1.0f);
} }
@@ -287,13 +372,16 @@ public final class IconRecentsView extends FrameLayout {
/** /**
* Get attached task item views ordered by most recent. * 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<TaskItemView> getTaskViews() {
int taskCount = mTaskRecyclerView.getChildCount(); int taskCount = mTaskRecyclerView.getChildCount();
TaskItemView[] itemViews = new TaskItemView[taskCount]; ArrayList<TaskItemView> itemViews = new ArrayList<>();
for (int i = 0; i < taskCount; i ++) { 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; return itemViews;
} }
@@ -303,12 +391,14 @@ public final class IconRecentsView extends FrameLayout {
* of tasks. * of tasks.
*/ */
private void updateContentViewVisibility() { private void updateContentViewVisibility() {
int taskListSize = mTaskLoader.getCurrentTaskList().size(); int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
if (mEmptyView.getVisibility() != VISIBLE && taskListSize == 0) { if (mShowingContentView != mEmptyView && taskListSize == 0) {
mShowingContentView = mEmptyView;
crossfadeViews(mEmptyView, mContentView); crossfadeViews(mEmptyView, mContentView);
mActivityHelper.leaveRecents(); mActivityHelper.leaveRecents();
} }
if (mContentView.getVisibility() != VISIBLE && taskListSize > 0) { if (mShowingContentView != mContentView && taskListSize > 0) {
mShowingContentView = mContentView;
crossfadeViews(mContentView, mEmptyView); crossfadeViews(mContentView, mEmptyView);
} }
} }
@@ -320,6 +410,7 @@ public final class IconRecentsView extends FrameLayout {
* @param fadeOutView view that should fade out * @param fadeOutView view that should fade out
*/ */
private void crossfadeViews(View fadeInView, View fadeOutView) { private void crossfadeViews(View fadeInView, View fadeOutView) {
fadeInView.animate().cancel();
fadeInView.setVisibility(VISIBLE); fadeInView.setVisibility(VISIBLE);
fadeInView.setAlpha(0f); fadeInView.setAlpha(0f);
fadeInView.animate() fadeInView.animate()
@@ -327,6 +418,7 @@ public final class IconRecentsView extends FrameLayout {
.setDuration(CROSSFADE_DURATION) .setDuration(CROSSFADE_DURATION)
.setListener(null); .setListener(null);
fadeOutView.animate().cancel();
fadeOutView.animate() fadeOutView.animate()
.alpha(0f) .alpha(0f)
.setDuration(CROSSFADE_DURATION) .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); * Start animating the layout animation where items fade in.
anim.addAnimation(alphaAnim); */
private void animateFadeInLayoutAnimation() {
LayoutAnimationController layoutAnim = new LayoutAnimationController(anim); if (mLayoutAnimation != null) {
layoutAnim.setDelay( // If layout animation still in progress, cancel and restart.
(float) LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN / LAYOUT_ITEM_ANIMATE_IN_DURATION); mLayoutAnimation.cancel();
}
return layoutAnim; ArrayList<TaskItemView> 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();
} }
} }
@@ -15,35 +15,66 @@
*/ */
package com.android.quickstep.views; package com.android.quickstep.views;
import static com.android.quickstep.views.TaskLayoutUtils.getTaskHeight;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.content.res.Configuration;
import android.graphics.Color; import android.content.res.Resources;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.android.launcher3.R; 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. * View representing an individual task item with the icon + thumbnail adjacent to the task label.
*/ */
public final class TaskItemView extends LinearLayout { public final class TaskItemView extends LinearLayout {
private static final String EMPTY_LABEL = "";
private static final String DEFAULT_LABEL = "..."; private static final String DEFAULT_LABEL = "...";
private final Drawable mDefaultIcon; private final Drawable mDefaultIcon;
private final Drawable mDefaultThumbnail;
private final TaskLayerDrawable mIconDrawable;
private final TaskLayerDrawable mThumbnailDrawable;
private TextView mLabelView; private TextView mLabelView;
private ImageView mIconView; private ImageView mIconView;
private ImageView mThumbnailView; 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<TaskItemView>("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) { public TaskItemView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
mDefaultIcon = context.getResources().getDrawable( Resources res = context.getResources();
android.R.drawable.sym_def_app_icon, context.getTheme()); 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 @Override
@@ -52,15 +83,28 @@ public final class TaskItemView extends LinearLayout {
mLabelView = findViewById(R.id.task_label); mLabelView = findViewById(R.id.task_label);
mThumbnailView = findViewById(R.id.task_thumbnail); mThumbnailView = findViewById(R.id.task_thumbnail);
mIconView = findViewById(R.id.task_icon); 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() { public void resetToEmptyUi() {
setLabel(DEFAULT_LABEL); mIconDrawable.resetDrawable();
setIcon(null); mThumbnailDrawable.resetDrawable();
setThumbnail(null); setLabel(EMPTY_LABEL);
} }
/** /**
@@ -69,11 +113,8 @@ public final class TaskItemView extends LinearLayout {
* @param label task label * @param label task label
*/ */
public void setLabel(@Nullable String label) { public void setLabel(@Nullable String label) {
if (label == null) { mLabelView.setText(getSafeLabel(label));
mLabelView.setText(DEFAULT_LABEL); // TODO: Animation for label
return;
}
mLabelView.setText(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 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 // 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. // view if we want the icon to be flush with the bottom of the thumbnail.
if (icon == null) { mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
mIconView.setImageDrawable(mDefaultIcon);
return;
}
mIconView.setImageDrawable(icon);
} }
/** /**
* Set the task thumbnail for the task. Sets to a default thumbnail if null. * 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) { public void setThumbnail(@Nullable ThumbnailData thumbnailData) {
if (thumbnail == null) { mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnailData));
mThumbnailView.setImageBitmap(null);
mThumbnailView.setBackgroundColor(Color.GRAY);
return;
}
mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
mThumbnailView.setImageBitmap(thumbnail);
} }
public View getThumbnailView() { public View getThumbnailView() {
return mThumbnailView; 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);
}
}
}
} }
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -18,6 +18,7 @@ package com.android.launcher3.shortcuts;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
@@ -55,7 +56,7 @@ public class DeepShortcutManager {
return false; return false;
} }
public void onShortcutsChanged(List<ShortcutInfoCompat> shortcuts) { public void onShortcutsChanged(List<ShortcutInfo> 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, * 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(). * because we only get "key" fields in onShortcutsChanged().
*/ */
public List<ShortcutInfoCompat> queryForFullDetails(String packageName, public List<ShortcutInfo> queryForFullDetails(String packageName,
List<String> shortcutIds, UserHandle user) { List<String> shortcutIds, UserHandle user) {
return Collections.emptyList(); return Collections.emptyList();
} }
@@ -73,7 +74,7 @@ public class DeepShortcutManager {
* Gets all the manifest and dynamic shortcuts associated with the given package and user, * Gets all the manifest and dynamic shortcuts associated with the given package and user,
* to be displayed in the shortcuts container on long press. * to be displayed in the shortcuts container on long press.
*/ */
public List<ShortcutInfoCompat> queryForShortcutsContainer(ComponentName activity, public List<ShortcutInfo> queryForShortcutsContainer(ComponentName activity,
UserHandle user) { UserHandle user) {
return Collections.emptyList(); return Collections.emptyList();
} }
@@ -96,7 +97,7 @@ public class DeepShortcutManager {
Bundle startActivityOptions, UserHandle user) { Bundle startActivityOptions, UserHandle user) {
} }
public Drawable getShortcutIconDrawable(ShortcutInfoCompat shortcutInfo, int density) { public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
return null; return null;
} }
@@ -105,16 +106,16 @@ public class DeepShortcutManager {
* *
* If packageName is null, returns all pinned shortcuts regardless of package. * If packageName is null, returns all pinned shortcuts regardless of package.
*/ */
public List<ShortcutInfoCompat> queryForPinnedShortcuts(String packageName, UserHandle user) { public List<ShortcutInfo> queryForPinnedShortcuts(String packageName, UserHandle user) {
return Collections.emptyList(); return Collections.emptyList();
} }
public List<ShortcutInfoCompat> queryForPinnedShortcuts(String packageName, public List<ShortcutInfo> queryForPinnedShortcuts(String packageName,
List<String> shortcutIds, UserHandle user) { List<String> shortcutIds, UserHandle user) {
return Collections.emptyList(); return Collections.emptyList();
} }
public List<ShortcutInfoCompat> queryForAllShortcuts(UserHandle user) { public List<ShortcutInfo> queryForAllShortcuts(UserHandle user) {
return Collections.emptyList(); return Collections.emptyList();
} }
@@ -0,0 +1,19 @@
<!--
Copyright (C) 2018 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="?android:attr/colorAccent" />
<corners android:radius="8dp" />
</shape>
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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.
-->
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="4dp"
android:background="@drawable/arrow_toast_rounded_background"
android:layout_gravity="center_horizontal"
android:elevation="2dp"
android:orientation="horizontal">
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:textColor="@android:color/white"
android:textSize="16sp"/>
<ImageView
android:id="@+id/dismiss"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:padding="10dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:alpha="0.7"
android:src="@drawable/ic_remove_no_shadow"
android:tint="@android:color/white"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/accessibility_close_task"/>
</LinearLayout>
<View
android:id="@+id/arrow"
android:elevation="2dp"
android:layout_width="10dp"
android:layout_height="8dp"
android:layout_marginTop="-2dp"
android:layout_gravity="center_horizontal"/>
</merge>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<com.android.launcher3.appprediction.PredictionRowView
android:id="@+id/prediction_row"
android:accessibilityPaneTitle="@string/title_app_suggestions"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.android.launcher3.appprediction.AppsDividerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/apps_divider_view" />
</merge>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:indeterminate="true"
android:indeterminateOnly="true"
android:indeterminateTint="?workspaceTextColor" />
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="chip_hint_foreground_color">#fff</color> <color name="chip_hint_foreground_color">#fff</color>
<color name="all_apps_label_text">#61000000</color>
<color name="all_apps_label_text_dark">#61FFFFFF</color>
<color name="all_apps_prediction_row_separator">#3c000000</color>
<color name="all_apps_prediction_row_separator_dark">#3cffffff</color>
</resources> </resources>
@@ -12,4 +12,13 @@
<dimen name="chip_text_top_padding">4dp</dimen> <dimen name="chip_text_top_padding">4dp</dimen>
<dimen name="chip_text_start_padding">10dp</dimen> <dimen name="chip_text_start_padding">10dp</dimen>
<dimen name="chip_text_size">14sp</dimen> <dimen name="chip_text_size">14sp</dimen>
<dimen name="all_apps_prediction_row_divider_height">17dp</dimen>
<dimen name="all_apps_label_top_padding">16dp</dimen>
<dimen name="all_apps_label_bottom_padding">8dp</dimen>
<dimen name="all_apps_label_text_size">14sp</dimen>
<dimen name="all_apps_tip_bottom_margin">8dp</dimen>
<!-- The size of corner radius of the arrow in the arrow toast. -->
<dimen name="arrow_toast_corner_radius">2dp</dimen>
</resources> </resources>
@@ -21,6 +21,8 @@
<string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string> <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
<string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
<string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string> <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
</resources> </resources>
@@ -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<Launcher, Boolean> onInitListener) {
super(onInitListener);
}
@Override
protected boolean init(Launcher launcher, boolean alreadyOnHome) {
PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW);
return super.init(launcher, alreadyOnHome);
}
}
@@ -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);
}
}
}
});
}
}
}
@@ -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<AppsDividerView> getTypeClass() {
return AppsDividerView.class;
}
}
@@ -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;
}
}
@@ -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<ShortcutKey, WorkspaceItemInfo> mShortcuts;
private final Map<String, InstantAppItemInfo> 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<ShortcutKey> shortcutKeys, List<String> 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<ShortcutKey> shortcutKeys = msg.obj != null ?
(List<ShortcutKey>) msg.obj : Collections.EMPTY_LIST;
Map<ShortcutKey, WorkspaceItemInfo> 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<String> pkgNames = msg.obj != null ?
(List<String>) msg.obj : Collections.EMPTY_LIST;
List<InstantAppItemInfo> 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<ShortcutKey, WorkspaceItemInfo>) msg.obj);
mOnUpdateCallback.run();
return true;
}
case UI_MSG_UPDATE_INSTANT_APPS: {
List<InstantAppItemInfo> instantAppItemInfos = (List<InstantAppItemInfo>) 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<ShortcutInfo> details = mgr.queryForFullDetails(
shortcutKey.componentName.getPackageName(),
Collections.<String>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<ResolveInfo> 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);
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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<PredictionRowView> TEXT_ALPHA =
new IntProperty<PredictionRowView>("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<ComponentKeyMapper> mPredictedAppComponents = new ArrayList<>();
// The set of predicted apps resolved from the component names and the current set of apps
private final ArrayList<ItemInfoWithIcon> 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<ItemInfoWithIcon> 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<ComponentKeyMapper> 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<ItemInfoWithIcon> processPredictedAppComponents(List<ComponentKeyMapper> components) {
if (getAppsStore().getApps().isEmpty()) {
// Apps have not been bound yet.
return Collections.emptyList();
}
List<ItemInfoWithIcon> 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<PredictionRowView> getTypeClass() {
return PredictionRowView.class;
}
}
@@ -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<PredictionUiStateManager> 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<AppTarget> 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<String> instantAppsToLoad = new ArrayList<>();
List<ShortcutKey> 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<ComponentKeyMapper> apps;
}
}
@@ -15,6 +15,8 @@
*/ */
package com.android.launcher3.uioverrides; 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 static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
@@ -22,15 +24,17 @@ import android.annotation.TargetApi;
import android.os.Build; import android.os.Build;
import android.util.FloatProperty; import android.util.FloatProperty;
import androidx.annotation.NonNull;
import com.android.launcher3.Launcher; import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherState;
import com.android.launcher3.LauncherStateManager.AnimationConfig; import com.android.launcher3.LauncherStateManager.AnimationConfig;
import com.android.launcher3.anim.AnimatorSetBuilder; 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.LauncherRecentsView;
import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsView;
import androidx.annotation.NonNull;
/** /**
* State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing * 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. * 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) { if (state.overviewUi) {
mRecentsView.updateEmptyMessage(); mRecentsView.updateEmptyMessage();
mRecentsView.resetTaskVisuals(); mRecentsView.resetTaskVisuals();
mRecentsView.setHintVisibility(1);
} else {
mRecentsView.setHintVisibility(0);
} }
setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state.getVisibleElements(mLauncher));
} }
@Override @Override
@@ -62,7 +64,6 @@ public final class RecentsViewStateController extends
if (!toState.overviewUi) { if (!toState.overviewUi) {
builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals); builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals);
mRecentsView.setHintVisibility(0);
} }
if (toState.overviewUi) { if (toState.overviewUi) {
@@ -74,8 +75,15 @@ public final class RecentsViewStateController extends
updateAnim.setDuration(config.duration); updateAnim.setDuration(config.duration);
builder.play(updateAnim); builder.play(updateAnim);
mRecentsView.updateEmptyMessage(); 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 @Override
@@ -18,11 +18,10 @@ package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
import android.os.RemoteException; import android.os.RemoteException;
import com.android.launcher3.Launcher; import com.android.launcher3.Launcher;
import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.quickstep.RecentsModel; 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.util.LayoutUtils;
import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -75,4 +74,8 @@ public class BackgroundAppState extends OverviewState {
return new ScaleAndTranslation(scale, 0f, 0f); return new ScaleAndTranslation(scale, 0f, 0f);
} }
@Override
public int getVisibleElements(Launcher launcher) {
return super.getVisibleElements(launcher) & ~RECENTS_CLEAR_ALL_BUTTON;
}
} }
@@ -17,8 +17,6 @@ package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
import static com.android.launcher3.anim.Interpolators.DEACCEL_2; 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.logging.LoggerUtils.newContainerTarget;
import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE; 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.AbstractFloatingView;
import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Launcher; import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherState;
import com.android.launcher3.R; import com.android.launcher3.R;
@@ -73,6 +70,15 @@ public class OverviewState extends LauncherState {
return new ScaleAndTranslation(scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor); 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 @Override
public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) { public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
return new ScaleAndTranslation(1f, 0f, 0f); return new ScaleAndTranslation(1f, 0f, 0f);
@@ -110,9 +116,9 @@ public class OverviewState extends LauncherState {
@Override @Override
public int getVisibleElements(Launcher launcher) { public int getVisibleElements(Launcher launcher) {
if (launcher.getDeviceProfile().isVerticalBarLayout()) { if (launcher.getDeviceProfile().isVerticalBarLayout()) {
return VERTICAL_SWIPE_INDICATOR; return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON;
} else { } else {
return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON |
(launcher.getAppsView().getFloatingHeaderView().hasVisibleContent() (launcher.getAppsView().getFloatingHeaderView().hasVisibleContent()
? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS); ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
} }
@@ -28,6 +28,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.ViewConfiguration;
import com.android.launcher3.Launcher; import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherState;
@@ -44,12 +45,14 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
private static final long PEEK_ANIM_DURATION = 100; private static final long PEEK_ANIM_DURATION = 100;
private final MotionPauseDetector mMotionPauseDetector; private final MotionPauseDetector mMotionPauseDetector;
private final float mMotionPauseMinDisplacement;
private AnimatorSet mPeekAnim; private AnimatorSet mPeekAnim;
public FlingAndHoldTouchController(Launcher l) { public FlingAndHoldTouchController(Launcher l) {
super(l, false /* allowDragToOverview */); super(l, false /* allowDragToOverview */);
mMotionPauseDetector = new MotionPauseDetector(l); mMotionPauseDetector = new MotionPauseDetector(l);
mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
} }
@Override @Override
@@ -98,7 +101,8 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
@Override @Override
public boolean onDrag(float displacement, MotionEvent event) { 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); return super.onDrag(displacement, event);
} }
@@ -111,6 +115,7 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
AnimatorSetBuilder builder = new AnimatorSetBuilder(); AnimatorSetBuilder builder = new AnimatorSetBuilder();
builder.setInterpolator(AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2); builder.setInterpolator(AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
builder.setInterpolator(AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE, OVERSHOOT_1_2);
AnimatorSet overviewAnim = mLauncher.getStateManager().createAtomicAnimation( AnimatorSet overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
NORMAL, OVERVIEW, builder, ANIM_ALL, ATOMIC_DURATION); NORMAL, OVERVIEW, builder, ANIM_ALL, ATOMIC_DURATION);
overviewAnim.addListener(new AnimatorListenerAdapter() { overviewAnim.addListener(new AnimatorListenerAdapter() {
@@ -17,8 +17,6 @@ package com.android.launcher3.uioverrides.touchcontrollers;
import static android.view.View.TRANSLATION_X; 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.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW; 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.AnimatorPlaybackController;
import com.android.launcher3.anim.AnimatorSetBuilder; import com.android.launcher3.anim.AnimatorSetBuilder;
import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.touch.SwipeDetector; import com.android.launcher3.touch.SwipeDetector;
import com.android.launcher3.userevent.nano.LauncherLogProto; 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.userevent.nano.LauncherLogProto.Action.Touch;
import com.android.launcher3.util.TouchController; import com.android.launcher3.util.TouchController;
import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsView;
@@ -212,11 +210,13 @@ public class NavBarToHomeTouchController implements TouchController, SwipeDetect
private void onSwipeInteractionCompleted(LauncherState targetState) { private void onSwipeInteractionCompleted(LauncherState targetState) {
clearState(); clearState();
mLauncher.getStateManager().goToState(targetState, false /* animated */); mLauncher.getStateManager().goToState(targetState, false /* animated */);
AccessibilityManagerCompat.sendStateEventToTest(mLauncher, targetState.ordinal);
} }
private void logStateChange(int startContainerType, int logAction) { private void logStateChange(int startContainerType, int logAction) {
mLauncher.getUserEventDispatcher().logStateChangeAction(logAction, mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
LauncherLogProto.Action.Direction.UP, LauncherLogProto.Action.Direction.UP,
mSwipeDetector.getDownX(), mSwipeDetector.getDownY(),
LauncherLogProto.ContainerType.NAVBAR, LauncherLogProto.ContainerType.NAVBAR,
startContainerType, startContainerType,
mEndState.containerType, mEndState.containerType,
@@ -75,7 +75,8 @@ public final class PortraitOverviewStateTouchHelper {
* @return the animation * @return the animation
*/ */
PendingAnimation createSwipeDownToTaskAppAnimation(long duration) { PendingAnimation createSwipeDownToTaskAppAnimation(long duration) {
TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getNextPage()); mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getCurrentPage());
if (taskView == null) { if (taskView == null) {
throw new IllegalStateException("There is no task view to animate to."); throw new IllegalStateException("There is no task view to animate to.");
} }
@@ -21,10 +21,11 @@ import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_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.UPLEFT;
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction.UPRIGHT; 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 static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.NAVBAR;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
@@ -35,12 +36,15 @@ import android.os.Bundle;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent; import android.view.MotionEvent;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.logging.UserEventDispatcher; import com.android.launcher3.logging.UserEventDispatcher;
import com.android.quickstep.util.MotionPauseDetector;
import com.android.systemui.shared.recents.ISystemUiProxy; 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; import com.android.systemui.shared.system.NavigationBarCompat;
/** /**
@@ -76,27 +80,32 @@ public class AssistantTouchConsumer implements InputConsumer {
private float mLastProgress; private float mLastProgress;
private int mState; private int mState;
private int mDirection; private int mDirection;
private ActivityControlHelper mActivityControlHelper;
private final float mDistThreshold; private final float mDistThreshold;
private final long mTimeThreshold; private final long mTimeThreshold;
private final int mAngleThreshold; private final int mAngleThreshold;
private final float mSlop; private final float mSlop;
private final MotionPauseDetector mMotionPauseDetector;
private final ISystemUiProxy mSysUiProxy; private final ISystemUiProxy mSysUiProxy;
private final InputConsumer mConsumerDelegate; private final InputConsumer mConsumerDelegate;
private final Context mContext; private final Context mContext;
private final InputMonitorCompat mInputMonitorCompat;
public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy, public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy,
InputConsumer delegate) { InputConsumer delegate, InputMonitorCompat inputMonitorCompat,
ActivityControlHelper activityControlHelper) {
final Resources res = context.getResources(); final Resources res = context.getResources();
mContext = context; mContext = context;
mSysUiProxy = systemUiProxy; mSysUiProxy = systemUiProxy;
mConsumerDelegate = delegate; mConsumerDelegate = delegate;
mMotionPauseDetector = new MotionPauseDetector(context);
mDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold); mDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold); mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold);
mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold); mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
mSlop = NavigationBarCompat.getQuickScrubTouchSlopPx(); mSlop = NavigationBarCompat.getQuickStepDragSlopPx();
mInputMonitorCompat = inputMonitorCompat;
mActivityControlHelper = activityControlHelper;
mState = STATE_INACTIVE; mState = STATE_INACTIVE;
} }
@@ -106,8 +115,18 @@ public class AssistantTouchConsumer implements InputConsumer {
} }
@Override @Override
public boolean isActive() { public boolean useSharedSwipeState() {
return mState != STATE_INACTIVE; if (mConsumerDelegate != null) {
return mConsumerDelegate.useSharedSwipeState();
}
return false;
}
@Override
public void onConsumerAboutToBeSwitched() {
if (mConsumerDelegate != null) {
mConsumerDelegate.onConsumerAboutToBeSwitched();
}
} }
@Override @Override
@@ -120,14 +139,6 @@ public class AssistantTouchConsumer implements InputConsumer {
mDownPos.set(ev.getX(), ev.getY()); mDownPos.set(ev.getX(), ev.getY());
mLastPos.set(mDownPos); mLastPos.set(mDownPos);
mTimeFraction = 0; mTimeFraction = 0;
// Detect when the gesture decelerates to start the assistant
mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
if (isPaused && mState == STATE_ASSISTANT_ACTIVE) {
mTimeFraction = 1;
updateAssistantProgress();
}
});
break; break;
} }
case ACTION_POINTER_UP: { case ACTION_POINTER_UP: {
@@ -156,6 +167,10 @@ public class AssistantTouchConsumer implements InputConsumer {
if (!mPassedSlop) { if (!mPassedSlop) {
// Normal gesture, ensure we pass the slop before we start tracking the gesture // 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) { if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) > mSlop) {
// Cancel touches to other windows (intercept)
mInputMonitorCompat.pilferPointers();
mPassedSlop = true; mPassedSlop = true;
mStartDragPos.set(mLastPos.x, mLastPos.y); mStartDragPos.set(mLastPos.x, mLastPos.y);
mDragTime = SystemClock.uptimeMillis(); mDragTime = SystemClock.uptimeMillis();
@@ -165,7 +180,8 @@ public class AssistantTouchConsumer implements InputConsumer {
Math.atan2(mDownPos.y - mLastPos.y, mDownPos.x - mLastPos.x)); Math.atan2(mDownPos.y - mLastPos.y, mDownPos.x - mLastPos.x));
mDirection = angle > 90 ? UPLEFT : UPRIGHT; mDirection = angle > 90 ? UPLEFT : UPRIGHT;
angle = angle > 90 ? 180 - angle : angle; angle = angle > 90 ? 180 - angle : angle;
if (angle > mAngleThreshold) {
if (angle > mAngleThreshold && angle < 90) {
mState = STATE_ASSISTANT_ACTIVE; mState = STATE_ASSISTANT_ACTIVE;
if (mConsumerDelegate != null) { if (mConsumerDelegate != null) {
@@ -183,7 +199,6 @@ public class AssistantTouchConsumer implements InputConsumer {
// Movement // Movement
mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x, mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
mLastPos.y - mStartDragPos.y); mLastPos.y - mStartDragPos.y);
mMotionPauseDetector.addPosition(mDistance, 0, ev.getEventTime());
if (mDistance >= 0) { if (mDistance >= 0) {
final long diff = SystemClock.uptimeMillis() - mDragTime; final long diff = SystemClock.uptimeMillis() - mDragTime;
mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1); mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
@@ -212,7 +227,8 @@ public class AssistantTouchConsumer implements InputConsumer {
animator.setInterpolator(Interpolators.DEACCEL_2); animator.setInterpolator(Interpolators.DEACCEL_2);
animator.start(); animator.start();
} }
mMotionPauseDetector.clear(); mPassedSlop = false;
mState = STATE_INACTIVE;
break; break;
} }
@@ -232,6 +248,14 @@ public class AssistantTouchConsumer implements InputConsumer {
SWIPE, mDirection, NAVBAR); SWIPE, mDirection, NAVBAR);
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE); 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); mSysUiProxy.startAssistant(args);
mLaunchedAssistant = true; mLaunchedAssistant = true;
} }
@@ -17,7 +17,9 @@ package com.android.quickstep;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.PointF;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.ViewConfiguration;
/** /**
* A dummy input consumer used when the device is still locked, e.g. from secure camera. * 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 { public class DeviceLockedInputConsumer implements InputConsumer {
private final Context mContext; private final Context mContext;
private final float mTouchSlopSquared;
private final PointF mTouchDown = new PointF();
public DeviceLockedInputConsumer(Context context) { public DeviceLockedInputConsumer(Context context) {
mContext = context; mContext = context;
float touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mTouchSlopSquared = touchSlop * touchSlop;
} }
@Override @Override
@@ -37,11 +43,19 @@ public class DeviceLockedInputConsumer implements InputConsumer {
@Override @Override
public void onMotionEvent(MotionEvent ev) { 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) { if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mContext.startActivity(new Intent(Intent.ACTION_MAIN) mTouchDown.set(x, y);
.addCategory(Intent.CATEGORY_HOME) } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 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));
}
} }
} }
} }
@@ -26,11 +26,13 @@ import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile;
import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.userevent.nano.LauncherLogProto; 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.LayoutUtils;
import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.quickstep.util.RemoteAnimationTargetSet;
import com.android.quickstep.views.RecentsView; 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.BiPredicate;
import java.util.function.Consumer; 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 * {@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 * currently running one and apps should interact with the {@link RecentsActivity} as opposed
@@ -72,7 +71,9 @@ public final class FallbackActivityControllerHelper implements
@Override @Override
public void onSwipeUpComplete(RecentsActivity activity) { public void onSwipeUpComplete(RecentsActivity activity) {
// TODO: RecentsView recentsView = activity.getOverviewPanel();
recentsView.getClearAllButton().setVisibilityAlpha(1);
recentsView.setDisallowScrollToClearAll(false);
} }
@Override @Override
@@ -121,6 +122,8 @@ public final class FallbackActivityControllerHelper implements
RecentsView rv = activity.getOverviewPanel(); RecentsView rv = activity.getOverviewPanel();
rv.setContentAlpha(0); rv.setContentAlpha(0);
rv.getClearAllButton().setVisibilityAlpha(0);
rv.setDisallowScrollToClearAll(true);
return new AnimationFactory() { return new AnimationFactory() {
@@ -34,7 +34,7 @@ public interface InputConsumer {
int getType(); int getType();
default boolean isActive() { default boolean useSharedSwipeState() {
return false; return false;
} }
@@ -38,10 +38,14 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.animation.Interpolator; 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.DeviceProfile;
import com.android.launcher3.Launcher; import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherInitListener; import com.android.launcher3.LauncherInitListenerEx;
import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherState;
import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.allapps.DiscoveryBounce;
import com.android.launcher3.anim.AnimatorPlaybackController; 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.BiPredicate;
import java.util.function.Consumer; import java.util.function.Consumer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
/** /**
* {@link ActivityControlHelper} for the in-launcher recents. * {@link ActivityControlHelper} for the in-launcher recents.
*/ */
@@ -163,21 +163,16 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
} }
activity.getStateManager().setRestState(resetState); activity.getStateManager().setRestState(resetState);
final LauncherState fromState; final LauncherState fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
if (!activityVisible) { activity.getStateManager().goToState(fromState, false);
// Since the launcher is not visible, we can safely reset the scroll position. // 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. // This ensures then the next swipe up to all-apps starts from scroll 0.
activity.getAppsView().reset(false /* animate */); activity.getAppsView().reset(false /* animate */);
fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
activity.getStateManager().goToState(fromState, false);
// Optimization, hide the all apps view to prevent layout while initializing // Optimization, hide the all apps view to prevent layout while initializing
activity.getAppsView().getContentView().setVisibility(View.GONE); activity.getAppsView().getContentView().setVisibility(View.GONE);
AccessibilityManagerCompat.sendStateEventToTest(activity, fromState.ordinal); AccessibilityManagerCompat.sendStateEventToTest(activity, fromState.ordinal);
} else {
fromState = startState;
}
return new AnimationFactory() { return new AnimationFactory() {
private Animator mShelfAnim; private Animator mShelfAnim;
@@ -185,8 +180,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
@Override @Override
public void createActivityController(long transitionLength) { public void createActivityController(long transitionLength) {
createActivityControllerInternal(activity, activityVisible, fromState, createActivityControllerInternal(activity, fromState, transitionLength, callback);
transitionLength, callback);
} }
@Override @Override
@@ -230,18 +224,9 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
}; };
} }
private void createActivityControllerInternal(Launcher activity, boolean wasVisible, private void createActivityControllerInternal(Launcher activity, LauncherState fromState,
LauncherState fromState, long transitionLength, long transitionLength, Consumer<AnimatorPlaybackController> callback) {
Consumer<AnimatorPlaybackController> callback) {
LauncherState endState = OVERVIEW; 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) { if (fromState == endState) {
return; return;
} }
@@ -319,7 +304,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
@Override @Override
public ActivityInitListener createActivityInitListener( public ActivityInitListener createActivityInitListener(
BiPredicate<Launcher, Boolean> onInitListener) { BiPredicate<Launcher, Boolean> onInitListener) {
return new LauncherInitListener(onInitListener); return new LauncherInitListenerEx(onInitListener);
} }
@Nullable @Nullable
@@ -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_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.ACTION_UP;
import static android.view.MotionEvent.INVALID_POINTER_ID; 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.ENTER;
import static com.android.launcher3.util.RaceConditionTracker.EXIT; 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.SysUINavigationMode.Mode.NO_BUTTON;
import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG; import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; 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.ViewConfiguration;
import android.view.WindowManager; import android.view.WindowManager;
import androidx.annotation.UiThread;
import com.android.launcher3.R;
import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RaceConditionTracker; import com.android.launcher3.util.RaceConditionTracker;
import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.TraceHelper;
@@ -62,8 +65,6 @@ import com.android.systemui.shared.system.WindowManagerWrapper;
import java.util.function.Consumer; import java.util.function.Consumer;
import androidx.annotation.UiThread;
/** /**
* Input consumer for handling events originating from an activity other than Launcher * 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<OtherActivityInputConsumer> mOnCompleteCallback; private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
private final MotionPauseDetector mMotionPauseDetector; private final MotionPauseDetector mMotionPauseDetector;
private final float mMotionPauseMinDisplacement;
private VelocityTracker mVelocityTracker; private VelocityTracker mVelocityTracker;
private WindowTransformSwipeHandler mInteractionHandler; 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. // Slop used to determine when we say that the gesture has started.
private boolean mPassedTouchSlop; 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 mStartDisplacement;
private float mStartDisplacementX;
private Handler mMainThreadHandler; private Handler mMainThreadHandler;
private Runnable mCancelRecentsAnimationRunnable = () -> { private Runnable mCancelRecentsAnimationRunnable = () -> {
@@ -131,6 +134,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
mMode = SysUINavigationMode.getMode(base); mMode = SysUINavigationMode.getMode(base);
mMotionPauseDetector = new MotionPauseDetector(base); mMotionPauseDetector = new MotionPauseDetector(base);
mMotionPauseMinDisplacement = base.getResources().getDimension(
R.dimen.motion_pause_detector_min_displacement_from_app);
mOnCompleteCallback = onCompleteCallback; mOnCompleteCallback = onCompleteCallback;
mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker = VelocityTracker.obtain();
mInputMonitorCompat = inputMonitorCompat; mInputMonitorCompat = inputMonitorCompat;
@@ -149,6 +154,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx(); mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx();
mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx(); mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx();
mPassedTouchSlop = mPassedDragSlop = continuingPreviousGesture; mPassedTouchSlop = mPassedDragSlop = continuingPreviousGesture;
} }
@@ -218,6 +224,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
} }
mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
float displacement = getDisplacement(ev); float displacement = getDisplacement(ev);
float displacementX = mLastPos.x - mDownPos.x;
if (!mPassedDragSlop) { if (!mPassedDragSlop) {
if (!mIsDeferredDownTarget) { if (!mIsDeferredDownTarget) {
@@ -226,13 +233,13 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
if (Math.abs(displacement) > mDragSlop) { if (Math.abs(displacement) > mDragSlop) {
mPassedDragSlop = true; mPassedDragSlop = true;
mStartDisplacement = displacement; mStartDisplacement = displacement;
mStartDisplacementX = displacementX;
} }
} }
} }
if (!mPassedTouchSlop) { if (!mPassedTouchSlop) {
if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) >= if (Math.hypot(displacementX, mLastPos.y - mDownPos.y) >= mTouchSlop) {
mTouchSlop) {
mPassedTouchSlop = true; mPassedTouchSlop = true;
if (mIsDeferredDownTarget) { if (mIsDeferredDownTarget) {
@@ -243,6 +250,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
if (!mPassedDragSlop) { if (!mPassedDragSlop) {
mPassedDragSlop = true; mPassedDragSlop = true;
mStartDisplacement = displacement; mStartDisplacement = displacement;
mStartDisplacementX = displacementX;
} }
notifyGestureStarted(); notifyGestureStarted();
} }
@@ -253,12 +261,12 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
mInteractionHandler.updateDisplacement(displacement - mStartDisplacement); mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
if (mMode == Mode.NO_BUTTON) { if (mMode == Mode.NO_BUTTON) {
boolean isLandscape = isNavBarOnLeft() || isNavBarOnRight(); float horizontalDist = Math.abs(displacementX - mStartDisplacementX);
float orthogonalDisplacement = !isLandscape float upDist = -(displacement - mStartDisplacement);
? ev.getX() - mDownPos.x boolean isLikelyToStartNewTask = horizontalDist > upDist;
: ev.getY() - mDownPos.y; mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
mMotionPauseDetector.addPosition(displacement, orthogonalDisplacement, || isLikelyToStartNewTask);
ev.getEventTime()); mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
} }
} }
break; break;
@@ -346,7 +354,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
: velocityY; : velocityY;
mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement); mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY)); mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY),
mDownPos);
} else { } else {
// Since we start touch tracking on DOWN, we may reach this state without actually // Since we start touch tracking on DOWN, we may reach this state without actually
// starting the gesture. In that case, just cleanup immediately. // starting the gesture. In that case, just cleanup immediately.
@@ -412,7 +421,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
} }
@Override @Override
public boolean isActive() { public boolean useSharedSwipeState() {
return mInteractionHandler != null; return mInteractionHandler != null;
} }
} }
@@ -36,6 +36,8 @@ import java.util.List;
*/ */
public class TaskOverlayFactory implements ResourceBasedOverride { 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. */ /** 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[]{ private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{
new TaskSystemShortcut.AppInfo(), new TaskSystemShortcut.AppInfo(),
@@ -38,7 +38,7 @@ import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher; import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherState;
import com.android.launcher3.R; 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.popup.SystemShortcut;
import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.InstantAppResolver; import com.android.launcher3.util.InstantAppResolver;
@@ -86,7 +86,7 @@ public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut
public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) { public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) {
Task task = view.getTask(); Task task = view.getTask();
ShortcutInfo dummyInfo = new ShortcutInfo(); WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
dummyInfo.intent = new Intent(); dummyInfo.intent = new Intent();
ComponentName component = task.getTopComponent(); ComponentName component = task.getTopComponent();
dummyInfo.intent.setComponent(component); dummyInfo.intent.setComponent(component);
@@ -87,7 +87,7 @@ public class TouchInteractionService extends Service implements
new LooperExecutor(UiThreadHelper.getBackgroundLooper()); new LooperExecutor(UiThreadHelper.getBackgroundLooper());
private static final String NAVBAR_VERTICAL_SIZE = "navigation_bar_frame_height"; 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 = public static final EventLogArray TOUCH_INTERACTION_LOG =
new EventLogArray("touch_interaction_log", 40); new EventLogArray("touch_interaction_log", 40);
@@ -422,7 +422,7 @@ public class TouchInteractionService extends Service implements
if (event.getAction() == ACTION_DOWN) { if (event.getAction() == ACTION_DOWN) {
if (isInValidSystemUiState() if (isInValidSystemUiState()
&& mSwipeTouchRegion.contains(event.getX(), event.getY())) { && mSwipeTouchRegion.contains(event.getX(), event.getY())) {
boolean useSharedState = mConsumer.isActive(); boolean useSharedState = mConsumer.useSharedSwipeState();
mConsumer.onConsumerAboutToBeSwitched(); mConsumer.onConsumerAboutToBeSwitched();
mConsumer = newConsumer(useSharedState, event); mConsumer = newConsumer(useSharedState, event);
TOUCH_INTERACTION_LOG.addLog("setInputConsumer", mConsumer.getType()); TOUCH_INTERACTION_LOG.addLog("setInputConsumer", mConsumer.getType());
@@ -455,13 +455,18 @@ public class TouchInteractionService extends Service implements
final ActivityControlHelper activityControl = final ActivityControlHelper activityControl =
mOverviewComponentObserver.getActivityControlHelper(); mOverviewComponentObserver.getActivityControlHelper();
if (runningTaskInfo == null && !mSwipeSharedState.goingToLauncher) { if (runningTaskInfo == null && !mSwipeSharedState.goingToLauncher) {
return InputConsumer.NO_OP; return InputConsumer.NO_OP;
} else if (mAssistantAvailable } else if (mAssistantAvailable
&& SysUINavigationMode.INSTANCE.get(this).getMode() == Mode.NO_BUTTON && SysUINavigationMode.INSTANCE.get(this).getMode() == Mode.NO_BUTTON
&& AssistantTouchConsumer.withinTouchRegion(this, event)) { && 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()) { } else if (mSwipeSharedState.goingToLauncher || activityControl.isResumed()) {
return OverviewInputConsumer.newInstance(activityControl, false); return OverviewInputConsumer.newInstance(activityControl, false);
} else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
@@ -64,6 +64,9 @@ import android.view.WindowInsets;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.Interpolator; import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile; 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.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
@TargetApi(Build.VERSION_CODES.O) @TargetApi(Build.VERSION_CODES.O)
public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
implements SwipeAnimationListener, OnApplyWindowInsetsListener { implements SwipeAnimationListener, OnApplyWindowInsetsListener {
@@ -253,6 +253,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
private boolean mGestureStarted; private boolean mGestureStarted;
private int mLogAction = Touch.SWIPE; private int mLogAction = Touch.SWIPE;
private int mLogDirection = Direction.UP; private int mLogDirection = Direction.UP;
private PointF mDownPos;
private final RecentsAnimationWrapper mRecentsAnimationWrapper; private final RecentsAnimationWrapper mRecentsAnimationWrapper;
@@ -429,8 +430,19 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
// If we've already ended the gesture and are going home, don't prepare recents UI, // 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. // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
if (mGestureEndTarget != HOME) { if (mGestureEndTarget != HOME) {
mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, Runnable initAnimFactory = () -> {
mWasLauncherAlreadyVisible, true, this::onAnimatorPlaybackControllerCreated); 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); AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible);
@@ -692,9 +704,10 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
/** /**
* @param endVelocity The velocity in the direction of the nav bar to the middle of the screen. * @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 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 @UiThread
public void onGestureEnded(float endVelocity, PointF velocity) { public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
float flingThreshold = mContext.getResources() float flingThreshold = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_velocity); .getDimension(R.dimen.quickstep_fling_threshold_velocity);
boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
@@ -707,6 +720,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
} else { } else {
mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT; mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT;
} }
mDownPos = downPos;
handleNormalGestureEnd(endVelocity, isFling, velocity); handleNormalGestureEnd(endVelocity, isFling, velocity);
} }
@@ -733,14 +747,10 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
float endShift; float endShift;
final float startShift; final float startShift;
Interpolator interpolator = DEACCEL; Interpolator interpolator = DEACCEL;
int nextPage = 0;
int taskToLaunch = 0;
final boolean goingToNewTask; final boolean goingToNewTask;
if (mRecentsView != null) { if (mRecentsView != null) {
nextPage = mRecentsView.getNextPage();
final int lastTaskIndex = mRecentsView.getTaskViewCount() - 1;
final int runningTaskIndex = mRecentsView.getRunningTaskIndex(); final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
taskToLaunch = nextPage <= lastTaskIndex ? nextPage : lastTaskIndex; final int taskToLaunch = mRecentsView.getNextPage();
goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex; goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
} else { } else {
goingToNewTask = false; goingToNewTask = false;
@@ -806,11 +816,6 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
} }
} }
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) { if (endTarget == HOME) {
setShelfState(ShelfAnimState.CANCEL, LINEAR, 0); setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
duration = Math.max(MIN_OVERSHOOT_DURATION, duration); duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
@@ -845,6 +850,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
: mRecentsView.getNextPage(); : mRecentsView.getNextPage();
UserEventDispatcher.newInstance(mContext).logStateChangeAction( UserEventDispatcher.newInstance(mContext).logStateChangeAction(
mLogAction, mLogDirection, mLogAction, mLogDirection,
(int) mDownPos.x, (int) mDownPos.y,
ContainerType.NAVBAR, ContainerType.APP, ContainerType.NAVBAR, ContainerType.APP,
endTarget.containerType, endTarget.containerType,
pageIndex); pageIndex);
@@ -1044,7 +1050,12 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
} }
private void invalidateHandlerWithLauncher() { private void invalidateHandlerWithLauncher() {
mLauncherTransitionController = null; if (mLauncherTransitionController != null) {
if (mLauncherTransitionController.getAnimationPlayer().isStarted()) {
mLauncherTransitionController.getAnimationPlayer().cancel();
}
mLauncherTransitionController = null;
}
mRecentsView.setEnableFreeScroll(true); mRecentsView.setEnableFreeScroll(true);
mRecentsView.setRunningTaskIconScaledDown(false); mRecentsView.setRunningTaskIconScaledDown(false);
@@ -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<ChipsContainer> HINT_VISIBILITY =
new FloatProperty<ChipsContainer>("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;
}
}
@@ -102,7 +102,7 @@ public class ClipAnimationHelper {
public ClipAnimationHelper(Context context) { public ClipAnimationHelper(Context context) {
mWindowCornerRadius = getWindowCornerRadius(context.getResources()); mWindowCornerRadius = getWindowCornerRadius(context.getResources());
mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources()); mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources());
mTaskCornerRadius = Themes.getDialogCornerRadius(context); mTaskCornerRadius = TaskCornerRadius.get(context);
} }
private void updateSourceStack(RemoteAnimationTargetCompat target) { private void updateSourceStack(RemoteAnimationTargetCompat target) {
@@ -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);
}
}
@@ -18,6 +18,7 @@ package com.android.quickstep.views;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Property;
import android.widget.Button; import android.widget.Button;
import com.android.launcher3.Utilities; import com.android.launcher3.Utilities;
@@ -26,8 +27,22 @@ import com.android.quickstep.views.RecentsView.ScrollState;
public class ClearAllButton extends Button implements PageCallbacks { public class ClearAllButton extends Button implements PageCallbacks {
public static final Property<ClearAllButton, Float> VISIBILITY_ALPHA =
new Property<ClearAllButton, Float>(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 mScrollAlpha = 1;
private float mContentAlpha = 1; private float mContentAlpha = 1;
private float mVisibilityAlpha = 1;
private final boolean mIsRtl; 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 @Override
public void onPageScroll(ScrollState scrollState) { public void onPageScroll(ScrollState scrollState) {
float width = getWidth(); float width = getWidth();
@@ -72,7 +94,7 @@ public class ClearAllButton extends Button implements PageCallbacks {
} }
private void updateAlpha() { private void updateAlpha() {
final float alpha = mScrollAlpha * mContentAlpha; final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha;
setAlpha(alpha); setAlpha(alpha);
setClickable(alpha == 1); setClickable(alpha == 1);
} }
@@ -32,7 +32,6 @@ import android.os.UserHandle;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
@@ -59,7 +58,6 @@ public final class DigitalWellBeingToast extends LinearLayout {
private static final String TAG = DigitalWellBeingToast.class.getSimpleName(); private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
private Task mTask; private Task mTask;
private ImageView mImage;
private TextView mText; private TextView mText;
public DigitalWellBeingToast(Context context, AttributeSet attrs) { public DigitalWellBeingToast(Context context, AttributeSet attrs) {
@@ -70,12 +68,15 @@ public final class DigitalWellBeingToast extends LinearLayout {
mLauncherApps = context.getSystemService(LauncherApps.class); mLauncherApps = context.getSystemService(LauncherApps.class);
} }
public TextView getTextView() {
return mText;
}
@Override @Override
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
mText = findViewById(R.id.digital_well_being_remaining_time); mText = findViewById(R.id.digital_well_being_remaining_time);
mImage = findViewById(R.id.digital_well_being_hourglass);
} }
public void initialize(Task task, InitializeCallback callback) { public void initialize(Task task, InitializeCallback callback) {
@@ -103,8 +104,6 @@ public final class DigitalWellBeingToast extends LinearLayout {
} else { } else {
setVisibility(VISIBLE); setVisibility(VISIBLE);
mText.setText(getText(appRemainingTimeMs)); mText.setText(getText(appRemainingTimeMs));
mImage.setImageResource(appRemainingTimeMs > 0 ?
R.drawable.hourglass_top : R.drawable.hourglass_bottom);
} }
callback.call(getContentDescriptionForTask( callback.call(getContentDescriptionForTask(
@@ -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.ALL_APPS_HEADER_EXTRA;
import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW; 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.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; 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 static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
@@ -39,11 +39,10 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherState;
import com.android.launcher3.R; import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.util.PendingAnimation; import com.android.launcher3.appprediction.PredictionUiStateManager;
import com.android.launcher3.views.BaseDragLayer; import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
import com.android.launcher3.views.ScrimView; import com.android.launcher3.views.ScrimView;
import com.android.quickstep.SysUINavigationMode; import com.android.quickstep.SysUINavigationMode;
import com.android.quickstep.hints.ChipsContainer;
import com.android.quickstep.util.ClipAnimationHelper; import com.android.quickstep.util.ClipAnimationHelper;
import com.android.quickstep.util.ClipAnimationHelper.TransformParams; import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.LayoutUtils;
@@ -55,7 +54,6 @@ import com.android.quickstep.util.LayoutUtils;
public class LauncherRecentsView extends RecentsView<Launcher> { public class LauncherRecentsView extends RecentsView<Launcher> {
private final TransformParams mTransformParams = new TransformParams(); private final TransformParams mTransformParams = new TransformParams();
private ChipsContainer mChipsContainer;
public LauncherRecentsView(Context context) { public LauncherRecentsView(Context context) {
this(context, null); this(context, null);
@@ -75,14 +73,6 @@ public class LauncherRecentsView extends RecentsView<Launcher> {
mActivity.getStateManager().goToState(NORMAL); 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 @Override
public void setTranslationY(float translationY) { public void setTranslationY(float translationY) {
super.setTranslationY(translationY); super.setTranslationY(translationY);
@@ -94,16 +84,6 @@ public class LauncherRecentsView extends RecentsView<Launcher> {
} }
} }
public void setHintVisibility(float v) {
if (mChipsContainer != null && ENABLE_HINTS_IN_OVERVIEW.get()) {
mChipsContainer.setHintVisibility(v);
}
}
public ChipsContainer getChipsContainer() {
return mChipsContainer;
}
@Override @Override
public void draw(Canvas canvas) { public void draw(Canvas canvas) {
maybeDrawEmptyMessage(canvas); maybeDrawEmptyMessage(canvas);
@@ -154,37 +134,6 @@ public class LauncherRecentsView extends RecentsView<Launcher> {
return anim; 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 @Override
protected void getTaskSize(DeviceProfile dp, Rect outRect) { protected void getTaskSize(DeviceProfile dp, Rect outRect) {
LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect); LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
@@ -247,4 +196,24 @@ public class LauncherRecentsView extends RecentsView<Launcher> {
} }
} }
} }
@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);
}
}
} }
@@ -18,6 +18,7 @@ package com.android.quickstep.views;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; 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.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;
import static com.android.launcher3.anim.Interpolators.ACCEL_2; import static com.android.launcher3.anim.Interpolators.ACCEL_2;
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 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.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; 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.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.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; 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 static com.android.quickstep.util.ClipAnimationHelper.TransformParams;
import android.animation.Animator; import android.animation.Animator;
@@ -46,6 +48,7 @@ import android.graphics.Canvas;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Handler; 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.PendingAnimation;
import com.android.launcher3.util.Themes; import com.android.launcher3.util.Themes;
import com.android.launcher3.util.ViewPool; import com.android.launcher3.util.ViewPool;
import com.android.quickstep.OverviewCallbacks;
import com.android.quickstep.RecentsAnimationWrapper; import com.android.quickstep.RecentsAnimationWrapper;
import com.android.quickstep.RecentsModel; import com.android.quickstep.RecentsModel;
import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener; import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener;
@@ -169,6 +171,7 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
private final ViewPool<TaskView> mTaskViewPool; private final ViewPool<TaskView> mTaskViewPool;
private boolean mDwbToastShown; private boolean mDwbToastShown;
private boolean mDisallowScrollToClearAll;
/** /**
* TODO: Call reloadIdNeeded in onTaskStackChanged. * TODO: Call reloadIdNeeded in onTaskStackChanged.
@@ -322,6 +325,8 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
mEmptyMessagePaint.setTextSize(getResources() mEmptyMessagePaint.setTextSize(getResources()
.getDimension(R.dimen.recents_empty_message_text_size)); .getDimension(R.dimen.recents_empty_message_text_size));
mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
Typeface.NORMAL));
mEmptyMessagePadding = getResources() mEmptyMessagePadding = getResources()
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
setWillNotDraw(false); setWillNotDraw(false);
@@ -748,8 +753,6 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
unloadVisibleTaskData(); unloadVisibleTaskData();
setCurrentPage(0); setCurrentPage(0);
OverviewCallbacks.get(getContext()).onResetOverview();
} }
/** /**
@@ -1159,6 +1162,7 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
@SuppressWarnings("unused") @SuppressWarnings("unused")
private void dismissAllTasks(View view) { private void dismissAllTasks(View view) {
runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
mActivity.getUserEventDispatcher().logActionOnControl(TAP, CLEAR_ALL_BUTTON);
} }
private void dismissCurrentTask() { private void dismissCurrentTask() {
@@ -1606,4 +1610,33 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
mRecentsAnimationWrapper.finish(toRecents, onFinishComplete); 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;
}
} }
@@ -48,6 +48,7 @@ import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes; import com.android.launcher3.util.Themes;
import com.android.quickstep.TaskOverlayFactory; import com.android.quickstep.TaskOverlayFactory;
import com.android.quickstep.TaskOverlayFactory.TaskOverlay; 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.Task;
import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract;
@@ -108,7 +109,7 @@ public class TaskThumbnailView extends View {
public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
mCornerRadius = Themes.getDialogCornerRadius(context); mCornerRadius = TaskCornerRadius.get(context);
mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this); mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
mPaint.setFilterBitmap(true); mPaint.setFilterBitmap(true);
mBackgroundPaint.setColor(Color.WHITE); mBackgroundPaint.setColor(Color.WHITE);
@@ -211,6 +211,10 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
return mMenuView; return mMenuView;
} }
public DigitalWellBeingToast getDigitalWellBeingToast() {
return mDigitalWellBeingToast;
}
/** /**
* Updates this task view to the given {@param task}. * Updates this task view to the given {@param task}.
*/ */
@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path android:pathData="M0,0H24V24H0Z M 0,0"/>
<path
android:fillColor="#FFFFFFFF"
android:pathData="M6,2V8H6l4,4L6,16H6v6H18V16h0l-4,-4,4,-4h0V2Zm6,9.5,-4,-4V4h8V7.5Z"/>
</group>
</vector>
@@ -2,7 +2,8 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24"
android:tint="?android:attr/textColorPrimary">
<group> <group>
<clip-path android:pathData="M0,0H24V24H0Z M 0,0"/> <clip-path android:pathData="M0,0H24V24H0Z M 0,0"/>
<path <path
+10 -3
View File
@@ -18,9 +18,16 @@
android:width="24.0dp" android:width="24.0dp"
android:height="24.0dp" android:height="24.0dp"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary">
<path <path
android:fillColor="#FFffffff" android:strokeColor="#FFFFFFFF"
android:pathData="M16,12L16,4l1,0L17,2L7,2l0,2l1,0l0,8l-2,2l0,2l5.2,0l0,6l1.6,0l0,-6L18,16l0,-2L16,12z"/> android:strokeAlpha=".008"
android:pathData="M0 0h24v24H0z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M17 4v7l2 3v2h-6v5l-1 1-1-1v-5H5v-2l2-3V4c0-1.1 0.9 -2 2-2h6c1.11 0 2 0.89 2
2zM9 4v7.75L7.5 14h9L15 11.75V4H9z" />
</vector> </vector>
+2 -1
View File
@@ -17,7 +17,8 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24"
android:tint="?android:attr/textColorPrimary">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
+1
View File
@@ -49,6 +49,7 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:src="@drawable/ic_hourglass_top"
/> />
<TextView <TextView
android:id="@+id/digital_well_being_remaining_time" android:id="@+id/digital_well_being_remaining_time"
+1 -1
View File
@@ -29,5 +29,5 @@
<!-- Assistant Gesture --> <!-- Assistant Gesture -->
<integer name="assistant_gesture_min_time_threshold">200</integer> <integer name="assistant_gesture_min_time_threshold">200</integer>
<integer name="assistant_gesture_corner_deg_threshold">30</integer> <integer name="assistant_gesture_corner_deg_threshold">20</integer>
</resources> </resources>
+5 -3
View File
@@ -19,6 +19,8 @@
<dimen name="task_thumbnail_top_margin">24dp</dimen> <dimen name="task_thumbnail_top_margin">24dp</dimen>
<dimen name="task_thumbnail_half_top_margin">12dp</dimen> <dimen name="task_thumbnail_half_top_margin">12dp</dimen>
<dimen name="task_thumbnail_icon_size">48dp</dimen> <dimen name="task_thumbnail_icon_size">48dp</dimen>
<!-- For screens without rounded corners -->
<dimen name="task_corner_radius_small">2dp</dimen>
<dimen name="recents_page_spacing">10dp</dimen> <dimen name="recents_page_spacing">10dp</dimen>
<dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen> <dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
@@ -36,7 +38,7 @@
<dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen> <dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen>
<dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen> <dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen>
<dimen name="motion_pause_detector_speed_fast">0.5dp</dimen> <dimen name="motion_pause_detector_speed_fast">0.5dp</dimen>
<dimen name="motion_pause_detector_min_displacement">48dp</dimen> <dimen name="motion_pause_detector_min_displacement_from_app">36dp</dimen>
<!-- Launcher app transition --> <!-- Launcher app transition -->
<dimen name="content_trans_y">50dp</dimen> <dimen name="content_trans_y">50dp</dimen>
@@ -63,8 +65,8 @@
<dimen name="shelf_surface_offset">24dp</dimen> <dimen name="shelf_surface_offset">24dp</dimen>
<!-- Assistant Gestures --> <!-- Assistant Gestures -->
<dimen name="gestures_assistant_size">28dp</dimen> <dimen name="gestures_assistant_size">48dp</dimen>
<dimen name="gestures_assistant_drag_threshold">70dp</dimen> <dimen name="gestures_assistant_drag_threshold">55dp</dimen>
<!-- Distance to move elements when swiping up to go home from launcher --> <!-- Distance to move elements when swiping up to go home from launcher -->
<dimen name="home_pullback_distance">28dp</dimen> <dimen name="home_pullback_distance">28dp</dimen>
+9
View File
@@ -58,4 +58,13 @@
<!-- Annotation shown on an app card in Recents, telling that the app has a usage limit set by <!-- Annotation shown on an app card in Recents, telling that the app has a usage limit set by
the user, and a given time is left for it today [CHAR LIMIT=22] --> the user, and a given time is left for it today [CHAR LIMIT=22] -->
<string name="time_left_for_app"><xliff:g id="time" example="7 minutes">%1$s</xliff:g> left today</string> <string name="time_left_for_app"><xliff:g id="time" example="7 minutes">%1$s</xliff:g> left today</string>
<!-- Accessibility title for the row of all-apps containing app predictions. [CHAR LIMIT=50] -->
<string name="title_app_suggestions">App suggestions</string>
<!-- Label for the header text of the All Apps section in All Apps view, used to separate Predicted Apps and Actions section from All Apps section. [CHAR_LIMIT=50] -->
<string name="all_apps_label">All apps</string>
<!-- Text of the tip when user lands in all apps view for the first time, indicating where the tip toast points to is the predicted apps section. [CHAR_LIMIT=50] -->
<string name="all_apps_prediction_tip">Your predicted apps</string>
</resources> </resources>
@@ -87,7 +87,7 @@ public class UiFactory extends RecentsUiFactory {
OverviewInteractionState.INSTANCE.get(launcher) OverviewInteractionState.INSTANCE.get(launcher)
.setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */); .setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */);
if (launcher != null && launcher.getDragLayer() != null) { if (launcher != null && launcher.getDragLayer() != null) {
launcher.getDragLayer().setDisallowBackGesture(shouldBackButtonBeHidden); launcher.getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
} }
} }
@@ -39,7 +39,5 @@ public class OverviewCallbacks implements ResourceBasedOverride {
public void onInitOverviewTransition() { } public void onInitOverviewTransition() { }
public void onResetOverview() { }
public void closeAllWindows() { } public void closeAllWindows() { }
} }
@@ -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_CHANGED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.content.Intent.ACTION_PACKAGE_REMOVED;
import static com.android.systemui.shared.system.PackageManagerWrapper import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
.ACTION_PREFERRED_ACTIVITY_CHANGED; import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
@@ -29,7 +29,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.os.PatternMatcher;
import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.PackageManagerWrapper;
@@ -109,13 +108,9 @@ public final class OverviewComponentObserver {
} }
mUpdateRegisteredPackage = defaultHome.getPackageName(); mUpdateRegisteredPackage = defaultHome.getPackageName();
IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED); mContext.registerReceiver(mOtherHomeAppUpdateReceiver, getPackageFilter(
updateReceiver.addAction(ACTION_PACKAGE_CHANGED); mUpdateRegisteredPackage, ACTION_PACKAGE_ADDED, ACTION_PACKAGE_CHANGED,
updateReceiver.addAction(ACTION_PACKAGE_REMOVED); ACTION_PACKAGE_REMOVED));
updateReceiver.addDataScheme("package");
updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage,
PatternMatcher.PATTERN_LITERAL);
mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver);
} }
} }
@@ -16,15 +16,15 @@
package com.android.quickstep; package com.android.quickstep;
import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources; import android.content.res.Resources;
import android.util.Log; import android.util.Log;
import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.systemui.shared.system.QuickStepContract;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -70,8 +70,6 @@ public class SysUINavigationMode {
mContext = context; mContext = context;
initializeMode(); initializeMode();
IntentFilter filter = new IntentFilter(ACTION_OVERLAY_CHANGED);
filter.addDataScheme("package");
mContext.registerReceiver(new BroadcastReceiver() { mContext.registerReceiver(new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@@ -81,7 +79,7 @@ public class SysUINavigationMode {
dispatchModeChange(); dispatchModeChange();
} }
} }
}, filter); }, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
} }
private void initializeMode() { private void initializeMode() {
@@ -111,6 +111,14 @@ public class TestInformationProvider extends ContentProvider {
response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) distance); response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) distance);
break; break;
} }
case TestProtocol.REQUEST_ENABLE_DRAG_LOGGING:
TestProtocol.sDebugTracing = true;
break;
case TestProtocol.REQUEST_DISABLE_DRAG_LOGGING:
TestProtocol.sDebugTracing = false;
break;
} }
return response; return response;
} }
@@ -36,17 +36,19 @@ import com.android.systemui.shared.system.MetricsLoggerCompat;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class UserEventDispatcherExtension extends UserEventDispatcher { public class UserEventDispatcherExtension extends UserEventDispatcher {
public static final int ALL_APPS_PREDICTION_TIPS = 2;
private static final String TAG = "UserEventDispatcher"; private static final String TAG = "UserEventDispatcher";
public UserEventDispatcherExtension(Context context) { } public UserEventDispatcherExtension(Context context) { }
public void logStateChangeAction(int action, int dir, int srcChildTargetType, public void logStateChangeAction(int action, int dir, int downX, int downY,
int srcParentContainerType, int dstContainerType, int srcChildTargetType, int srcParentContainerType,
int pageIndex) { int dstContainerType, int pageIndex) {
new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY, new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY,
dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER); dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER);
super.logStateChangeAction(action, dir, srcChildTargetType, srcParentContainerType, super.logStateChangeAction(action, dir, downX, downY, srcChildTargetType,
dstContainerType, pageIndex); srcParentContainerType, dstContainerType, pageIndex);
} }
public void logActionTip(int actionType, int viewType) { public void logActionTip(int actionType, int viewType) {
@@ -43,8 +43,6 @@ public class LayoutUtils {
float extraSpace; float extraSpace;
if (dp.isVerticalBarLayout()) { if (dp.isVerticalBarLayout()) {
extraSpace = 0; extraSpace = 0;
} else if (FeatureFlags.ENABLE_HINTS_IN_OVERVIEW.get()){
extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx + dp.chipHintHeightPx;
} else { } else {
extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx; extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx;
} }
@@ -38,28 +38,26 @@ public class MotionPauseDetector {
private final float mSpeedVerySlow; private final float mSpeedVerySlow;
private final float mSpeedSomewhatFast; private final float mSpeedSomewhatFast;
private final float mSpeedFast; private final float mSpeedFast;
private final float mMinDisplacementForPause;
private final Alarm mForcePauseTimeout; private final Alarm mForcePauseTimeout;
private Long mPreviousTime = null; private Long mPreviousTime = null;
private Float mPreviousPosition = null; private Float mPreviousPosition = null;
private Float mPreviousVelocity = null; private Float mPreviousVelocity = null;
private TotalDisplacement mTotalDisplacement = new TotalDisplacement();
private Float mFirstPosition = null; private Float mFirstPosition = null;
private Float mFirstOrthogonalPosition = null;
private OnMotionPauseListener mOnMotionPauseListener; private OnMotionPauseListener mOnMotionPauseListener;
private boolean mIsPaused; private boolean mIsPaused;
// Bias more for the first pause to make it feel extra responsive. // Bias more for the first pause to make it feel extra responsive.
private boolean mHasEverBeenPaused; private boolean mHasEverBeenPaused;
/** @see #setDisallowPause(boolean) */
private boolean mDisallowPause;
public MotionPauseDetector(Context context) { public MotionPauseDetector(Context context) {
Resources res = context.getResources(); Resources res = context.getResources();
mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow); mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast); mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_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 = new Alarm();
mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */)); mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
} }
@@ -72,29 +70,31 @@ public class MotionPauseDetector {
mOnMotionPauseListener = listener; 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. * Computes velocity and acceleration to determine whether the motion is paused.
* @param position The x or y component of the motion being tracked. * @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)}. * 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) { if (mFirstPosition == null) {
mFirstPosition = position; mFirstPosition = position;
} }
if (mFirstOrthogonalPosition == null) {
mFirstOrthogonalPosition = orthogonalPosition;
}
mForcePauseTimeout.setAlarm(FORCE_PAUSE_TIMEOUT); mForcePauseTimeout.setAlarm(FORCE_PAUSE_TIMEOUT);
if (mPreviousTime != null && mPreviousPosition != null) { if (mPreviousTime != null && mPreviousPosition != null) {
long changeInTime = Math.max(1, time - mPreviousTime); long changeInTime = Math.max(1, time - mPreviousTime);
float changeInPosition = position - mPreviousPosition; float changeInPosition = position - mPreviousPosition;
float velocity = changeInPosition / changeInTime; float velocity = changeInPosition / changeInTime;
if (mPreviousVelocity != null) { if (mPreviousVelocity != null) {
mTotalDisplacement.set(Math.abs(position - mFirstPosition), checkMotionPaused(velocity, mPreviousVelocity);
Math.abs(orthogonalPosition - mFirstOrthogonalPosition));
checkMotionPaused(velocity, mPreviousVelocity, mTotalDisplacement);
} }
mPreviousVelocity = velocity; mPreviousVelocity = velocity;
} }
@@ -102,8 +102,7 @@ public class MotionPauseDetector {
mPreviousPosition = position; mPreviousPosition = position;
} }
private void checkMotionPaused(float velocity, float prevVelocity, private void checkMotionPaused(float velocity, float prevVelocity) {
TotalDisplacement totalDisplacement) {
float speed = Math.abs(velocity); float speed = Math.abs(velocity);
float previousSpeed = Math.abs(prevVelocity); float previousSpeed = Math.abs(prevVelocity);
boolean isPaused; 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); updatePaused(isPaused);
} }
private void updatePaused(boolean isPaused) { private void updatePaused(boolean isPaused) {
if (mDisallowPause) {
isPaused = false;
}
if (mIsPaused != isPaused) { if (mIsPaused != isPaused) {
mIsPaused = isPaused; mIsPaused = isPaused;
if (mIsPaused) { if (mIsPaused) {
@@ -151,8 +147,6 @@ public class MotionPauseDetector {
mPreviousPosition = null; mPreviousPosition = null;
mPreviousVelocity = null; mPreviousVelocity = null;
mFirstPosition = null; mFirstPosition = null;
mFirstOrthogonalPosition = null;
mTotalDisplacement.set(0, 0);
setOnMotionPauseListener(null); setOnMotionPauseListener(null);
mIsPaused = mHasEverBeenPaused = false; mIsPaused = mHasEverBeenPaused = false;
mForcePauseTimeout.cancelAlarm(); mForcePauseTimeout.cancelAlarm();
@@ -165,18 +159,4 @@ public class MotionPauseDetector {
public interface OnMotionPauseListener { public interface OnMotionPauseListener {
void onMotionPauseChanged(boolean isPaused); 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;
}
}
} }
@@ -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<LauncherActivityInfo> 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<BubbleTextView> getPredictedApp(Launcher launcher) {
PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
.findFixedRowByType(PredictionRowView.class);
ArrayList<BubbleTextView> 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<AppTarget> 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;
});
}
}
@@ -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.<RecentsView>getOverviewPanel().getTaskViewAt(0);
}
private void runWithShellPermission(Runnable action) {
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
try {
action.run();
} finally {
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
}
}
@@ -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.ALL;
import static com.android.quickstep.NavigationModeSwitchRule.Mode.THREE_BUTTON; 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.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_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_3BUTTON_OVERLAY;
import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_GESTURAL_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 = final LauncherInstrumentation.NavigationModel originalMode =
mLauncher.getNavigationModel(); mLauncher.getNavigationModel();
try { try {
// if (mode == ZERO_BUTTON || mode == ALL) { if (mode == ZERO_BUTTON || mode == ALL) {
// evaluateWithZeroButtons(); evaluateWithZeroButtons();
// } }
if (mode == TWO_BUTTON || mode == ALL) { if (mode == TWO_BUTTON || mode == ALL) {
evaluateWithTwoButtons(); evaluateWithTwoButtons();
} }
@@ -131,7 +132,10 @@ public class NavigationModeSwitchRule implements TestRule {
overlayPackage == NAV_BAR_MODE_GESTURAL_OVERLAY); overlayPackage == NAV_BAR_MODE_GESTURAL_OVERLAY);
for (int i = 0; i != 100; ++i) { for (int i = 0; i != 100; ++i) {
if (mLauncher.getNavigationModel() == expectedMode) return; if (mLauncher.getNavigationModel() == expectedMode) {
Thread.sleep(1000);
return;
}
Thread.sleep(100); Thread.sleep(100);
} }
Assert.fail("Couldn't switch to " + overlayPackage); Assert.fail("Couldn't switch to " + overlayPackage);
@@ -61,7 +61,7 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest {
} }
@Test @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) @NavigationModeSwitch(mode = Mode.TWO_BUTTON)
public void testPressHome() { public void testPressHome() {
runTest(enterEvt(Launcher.ON_CREATE_EVT), runTest(enterEvt(Launcher.ON_CREATE_EVT),
@@ -75,14 +75,6 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest {
exitEvt(Launcher.ON_CREATE_EVT)); 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 @Test
@NavigationModeSwitch @NavigationModeSwitch
public void testStressPressHome() { public void testStressPressHome() {
@@ -16,6 +16,8 @@
package com.android.quickstep; package com.android.quickstep;
import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -60,7 +62,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
} }
private void startTestApps() throws Exception { private void startTestApps() throws Exception {
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_BROWSER)); startAppFast(getAppPackageName());
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CONTACTS)); startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CONTACTS));
@@ -72,7 +74,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
@Test @Test
@PortraitLandscape @PortraitLandscape
@Ignore @Ignore // Enable after b/131115533
public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException { public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException {
mDevice.pressRecentApps(); mDevice.pressRecentApps();
waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW); waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW);
-23
View File
@@ -1,23 +0,0 @@
<!-- Copyright (C) 2017 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
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" />
</vector>
+6 -3
View File
@@ -17,8 +17,11 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0"
android:tint="?android:attr/textColorHint" >
<path <path
android:pathData="M20,6h-4V4c0,-1.11 -0.89,-2 -2,-2h-4C8.89,2 8,2.89 8,4v2H4C2.89,6 2.01,6.89 2.01,8L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V8C22,6.89 21.11,6 20,6zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S13.1,15 12,15zM14,6h-4V4h4V6z" 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
android:fillColor="?android:attr/textColorHint"/> 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"/>
</vector> </vector>
@@ -22,7 +22,6 @@
android:tint="?android:attr/textColorHint" > android:tint="?android:attr/textColorHint" >
<path <path
android:pathData="M19,9H5c-0.55,0-1,0.45-1,1l0,0c0,0.55,0.45,1,1,1h14c0.55,0,1-0.45,1-1l0,0C20,9.45,19.55,9,19,9z M5, android:pathData="M20,9H4v2h16V9z M4,15h16v-2H4V15z"
15h14c0.55,0,1-0.45,1-1l0,0c0-0.55-0.45-1-1-1H5c-0.55,0-1,0.45-1,1l0,0C4,14.55,4.45,15,5,15z"
android:fillColor="@android:color/white" /> android:fillColor="@android:color/white" />
</vector> </vector>
+3 -3
View File
@@ -21,12 +21,12 @@
android:tint="?android:attr/textColorPrimary"> android:tint="?android:attr/textColorPrimary">
<path <path
android:fillColor="#FFFFFF" android:fillColor="@android:color/white"
android:pathData="M 11 7 H 13 V 9 H 11 V 7 Z" /> android:pathData="M 11 7 H 13 V 9 H 11 V 7 Z" />
<path <path
android:fillColor="#FFFFFF" android:fillColor="@android:color/white"
android:pathData="M 11 11 H 13 V 17 H 11 V 11 Z" /> android:pathData="M 11 11 H 13 V 17 H 11 V 11 Z" />
<path <path
android:fillColor="#FFFFFF" android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M12,20c-4.41,0-8-3.59-8-8 c0-4.41,3.59-8,8-8s8,3.59,8,8C20,16.41,16.41,20,12,20z" /> android:pathData="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M12,20c-4.41,0-8-3.59-8-8 c0-4.41,3.59-8,8-8s8,3.59,8,8C20,16.41,16.41,20,12,20z" />
</vector> </vector>
+6 -4
View File
@@ -17,11 +17,13 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24"
android:tint="?android:attr/textColorPrimary">
<path <path
android:fillColor="?android:attr/textColorPrimary" android:fillColor="@android:color/white"
android:pathData="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" /> android:pathData="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z" />
<path <path
android:pathData="M0 0h24v24H0z" /> 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" />
</vector> </vector>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/textColorPrimary">
<path
android:fillColor="@android:color/white"
android:strokeWidth="1"
android:pathData="M10,20 C4.49,20 0,15.51 0,10 C0,4.49 4.49,0 10,0 C15.51,0 20,4.04 20,9
C20,12.31 17.31,15 14,15 L12.23,15 C11.95,15 11.73,15.22 11.73,15.5 C11.73,15.62
11.78,15.73 11.86,15.83 C12.27,16.3 12.5,16.89 12.5,17.5 C12.5,18.88 11.38,20
10,20 Z M10,2 C5.59,2 2,5.59 2,10 C2,14.41 5.59,18 10,18 C10.28,18 10.5,17.78
10.5,17.5 C10.5,17.34 10.42,17.22 10.36,17.15 C9.95,16.69 9.73,16.1 9.73,15.5
C9.73,14.12 10.85,13 12.23,13 L14,13 C16.21,13 18,11.21 18,9 C18,5.14 14.41,2
10,2 Z M4.5,11 C5.32842712,11 6,10.3284271 6,9.5 C6,8.67157288 5.32842712,8
4.5,8 C3.67157288,8 3,8.67157288 3,9.5 C3,10.3284271 3.67157288,11 4.5,11 Z
M7.5,7 C8.32842712,7 9,6.32842712 9,5.5 C9,4.67157288 8.32842712,4 7.5,4
C6.67157288,4 6,4.67157288 6,5.5 C6,6.32842712 6.67157288,7 7.5,7 Z M12.5,7
C13.3284271,7 14,6.32842712 14,5.5 C14,4.67157288 13.3284271,4 12.5,4
C11.6715729,4 11,4.67157288 11,5.5 C11,6.32842712 11.6715729,7 12.5,7 Z M15.5,11
C16.3284271,11 17,10.3284271 17,9.5 C17,8.67157288 16.3284271,8 15.5,8
C14.6715729,8 14,8.67157288 14,9.5 C14,10.3284271 14.6715729,11 15.5,11 Z" />
</vector>
+7 -9
View File
@@ -14,14 +14,12 @@
limitations under the License. limitations under the License.
--> -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24.0" android:viewportHeight="24.0"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:tint="?android:attr/textColorPrimary" > android:tint="?android:attr/textColorPrimary">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="@android:color/white"
android:pathData="M13.41,12l5.29-5.29c0.39-0.39,0.39-1.02,0-1.41c-0.39-0.39-1.02-0.39-1.41,0L12,10.59L6.71, 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" />
5.29c-0.39-0.39-1.02-0.39-1.41,0c-0.39,0.39-0.39,1.02,0,1.41L10.59,12l-5.29,5.29c-0.39,0.39-0.39,1.02,
0,1.41c0.39,0.39,1.02,0.39,1.41,0L12,13.41l5.29,5.29c0.39,0.39,1.02,0.39,1.41,0c0.39-0.39,0.39-1.02,0-1.41L13.41,12z"/>
</vector> </vector>
+2 -2
View File
@@ -20,7 +20,7 @@ Copyright (C) 2016 The Android Open Source Project
android:viewportHeight="24.0" android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary" > android:tint="?android:attr/textColorPrimary" >
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="@android:color/white"
android:pathData="M13.85,22.25h-3.7c-0.74,0-1.36-0.54-1.45-1.27l-0.27-1.89c-0.27-0.14-0.53-0.29-0.79-0.46l-1.8,0.72 android:pathData="M13.85,22.25h-3.7c-0.74,0-1.36-0.54-1.45-1.27l-0.27-1.89c-0.27-0.14-0.53-0.29-0.79-0.46l-1.8,0.72
c-0.7,0.26-1.47-0.03-1.81-0.65L2.2,15.53c-0.35-0.66-0.2-1.44,0.36-1.88l1.53-1.19c-0.01-0.15-0.02-0.3-0.02-0.46 c-0.7,0.26-1.47-0.03-1.81-0.65L2.2,15.53c-0.35-0.66-0.2-1.44,0.36-1.88l1.53-1.19c-0.01-0.15-0.02-0.3-0.02-0.46
c0-0.15,0.01-0.31,0.02-0.46l-1.52-1.19C1.98,9.9,1.83,9.09,2.2,8.47l1.85-3.19c0.34-0.62,1.11-0.9,1.79-0.63l1.81,0.73 c0-0.15,0.01-0.31,0.02-0.46l-1.52-1.19C1.98,9.9,1.83,9.09,2.2,8.47l1.85-3.19c0.34-0.62,1.11-0.9,1.79-0.63l1.81,0.73
@@ -38,6 +38,6 @@ Copyright (C) 2016 The Android Open Source Project
M18.22,6.27c0,0.01,0.01,0.02,0.01,0.02L18.22,6.27z M5.79,6.25L5.78,6.27C5.78,6.27,5.79,6.26,5.79,6.25z M13.31,3.28 M18.22,6.27c0,0.01,0.01,0.02,0.01,0.02L18.22,6.27z M5.79,6.25L5.78,6.27C5.78,6.27,5.79,6.26,5.79,6.25z M13.31,3.28
c0,0.01,0,0.01,0,0.02L13.31,3.28z M10.69,3.26l0,0.02C10.69,3.27,10.69,3.27,10.69,3.26z"/> c0,0.01,0,0.01,0,0.02L13.31,3.28z M10.69,3.26l0,0.02C10.69,3.27,10.69,3.27,10.69,3.26z"/>
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="@android:color/white"
android:pathData="M8.5,12a3.5,3.5 0 1,0 7,0a3.5,3.5 0 1,0 -7,0"/> android:pathData="M8.5,12a3.5,3.5 0 1,0 7,0a3.5,3.5 0 1,0 -7,0"/>
</vector> </vector>
+3 -3
View File
@@ -20,12 +20,12 @@
android:viewportHeight="24.0" android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary" > android:tint="?android:attr/textColorPrimary" >
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="@android:color/white"
android:pathData="M15,4V3H9v1H4v2h1v13c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V6h1V4H15z M17,19H7V6h10V19z" /> android:pathData="M15,4V3H9v1H4v2h1v13c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V6h1V4H15z M17,19H7V6h10V19z" />
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="@android:color/white"
android:pathData="M 9 8 H 11 V 17 H 9 V 8 Z" /> android:pathData="M 9 8 H 11 V 17 H 9 V 8 Z" />
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="@android:color/white"
android:pathData="M 13 8 H 15 V 17 H 13 V 8 Z" /> android:pathData="M 13 8 H 15 V 17 H 13 V 8 Z" />
</vector> </vector>
+4 -3
View File
@@ -1,5 +1,5 @@
<!-- <!--
Copyright (C) 2016 The Android Open Source Project Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -17,9 +17,10 @@ Copyright (C) 2016 The Android Open Source Project
android:width="@dimen/options_menu_icon_size" android:width="@dimen/options_menu_icon_size"
android:height="@dimen/options_menu_icon_size" android:height="@dimen/options_menu_icon_size"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary">
<path <path
android:fillColor="?android:attr/textColorPrimary" android:fillColor="@android:color/white"
android:pathData="M9,12.71l2.14,2.58l3-3.87L18,16.57H6L9,12.71z M5,5h6V3H5C3.9,3,3,3.9,3,5v6h2V5z M19,19h-6v2h6c1.1,0,2-0.9,2-2v-6h-2V19z android:pathData="M9,12.71l2.14,2.58l3-3.87L18,16.57H6L9,12.71z M5,5h6V3H5C3.9,3,3,3.9,3,5v6h2V5z M19,19h-6v2h6c1.1,0,2-0.9,2-2v-6h-2V19z
M5,19v-6H3v6c0,1.1,0.9,2,2,2h6v-2H5z M19,5v6h2V5c0-1.1-0.9-2-2-2h-6v2H19z M16,9c0.55,0,1-0.45,1-1s-0.45-1-1-1 M5,19v-6H3v6c0,1.1,0.9,2,2,2h6v-2H5z M19,5v6h2V5c0-1.1-0.9-2-2-2h-6v2H19z M16,9c0.55,0,1-0.45,1-1s-0.45-1-1-1
c-0.55,0-1,0.45-1,1S15.45,9,16,9z"/> c-0.55,0-1,0.45-1,1S15.45,9,16,9z"/>
+8 -4
View File
@@ -20,8 +20,12 @@
android:viewportHeight="24.0" android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary" > android:tint="?android:attr/textColorPrimary" >
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="@android:color/white"
android:pathData="M22.85,19.39L12.93,2.25c-0.41-0.71-1.44-0.71-1.85,0L1.15,19.39C0.73,20.11,1.25,21,2.07,21h19.85 android:pathData="M12,5.99L19.53,19H4.47L12,5.99 M12,2L1,21h22L12,2L12,2z" />
C22.75,21,23.27,20.11,22.85,19.39z M11,10c0-0.55,0.45-1,1-1s1,0.45,1,1v3c0,0.55-0.45,1-1,1s-1-0.45-1-1V10z M12,18.2 <path
c-0.61,0-1.1-0.49-1.1-1.1S11.39,16,12,16s1.1,0.49,1.1,1.1C13.1,17.71,12.61,18.2,12,18.2z"/> android:fillColor="@android:color/white"
android:pathData="M 13 16 L 11 16 L 11 18 L 13 18 L 13 16 Z" />
<path
android:fillColor="@android:color/white"
android:pathData="M 13 10 L 11 10 L 11 14 L 13 14 L 13 10 Z" />
</vector> </vector>
+3 -2
View File
@@ -17,9 +17,10 @@ Copyright (C) 2016 The Android Open Source Project
android:width="@dimen/options_menu_icon_size" android:width="@dimen/options_menu_icon_size"
android:height="@dimen/options_menu_icon_size" android:height="@dimen/options_menu_icon_size"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary">
<path <path
android:fillColor="?android:attr/textColorPrimary" android:fillColor="@android:color/white"
android:pathData="M16.66,4.52l2.83,2.83l-2.83,2.83l-2.83-2.83L16.66,4.52 M9,5v4H5V5H9 M19,15v4h-4v-4H19 M9,15v4H5v-4H9 M16.66,1.69 android:pathData="M16.66,4.52l2.83,2.83l-2.83,2.83l-2.83-2.83L16.66,4.52 M9,5v4H5V5H9 M19,15v4h-4v-4H19 M9,15v4H5v-4H9 M16.66,1.69
L11,7.34L16.66,13l5.66-5.66L16.66,1.69L16.66,1.69z M11,3H3v8h8V7.34V3L11,3z M21,13h-4.34H13v8h8V13L21,13z M11,13H3v8h8V13L11,13z"/> L11,7.34L16.66,13l5.66-5.66L16.66,1.69L16.66,1.69z M11,3H3v8h8V7.34V3L11,3z M21,13h-4.34H13v8h8V13L21,13z M11,13H3v8h8V13L11,13z"/>
</vector> </vector>
+3 -4
View File
@@ -24,7 +24,7 @@
android:clipChildren="true" android:clipChildren="true"
android:clipToPadding="false" android:clipToPadding="false"
android:focusable="false" android:focusable="false"
android:saveEnabled="false" > android:saveEnabled="false">
<include <include
layout="@layout/all_apps_rv_layout" layout="@layout/all_apps_rv_layout"
@@ -47,7 +47,8 @@
android:layout_height="@dimen/all_apps_header_tab_height" android:layout_height="@dimen/all_apps_header_tab_height"
android:layout_marginLeft="@dimen/all_apps_tabs_side_padding" android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
android:layout_marginRight="@dimen/all_apps_tabs_side_padding" android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
android:orientation="horizontal"> android:orientation="horizontal"
style="@style/TextHeadline">
<Button <Button
android:id="@+id/tab_personal" android:id="@+id/tab_personal"
@@ -55,7 +56,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:fontFamily="sans-serif-medium"
android:text="@string/all_apps_personal_tab" android:text="@string/all_apps_personal_tab"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="@color/all_apps_tab_text" android:textColor="@color/all_apps_tab_text"
@@ -67,7 +67,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:fontFamily="sans-serif-medium"
android:text="@string/all_apps_work_tab" android:text="@string/all_apps_work_tab"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="@color/all_apps_tab_text" android:textColor="@color/all_apps_tab_text"
+1 -2
View File
@@ -29,11 +29,10 @@
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:paddingStart="@dimen/deep_shortcuts_text_padding_start" android:paddingStart="@dimen/deep_shortcuts_text_padding_start"
android:paddingEnd="@dimen/popup_padding_end" android:paddingEnd="@dimen/popup_padding_end"
android:drawableEnd="@drawable/deep_shortcuts_drag_handle" android:drawableEnd="@drawable/ic_drag_handle"
android:drawablePadding="@dimen/deep_shortcut_drawable_padding" android:drawablePadding="@dimen/deep_shortcut_drawable_padding"
android:textSize="14sp" android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:fontFamily="sans-serif"
launcher:layoutHorizontal="true" launcher:layoutHorizontal="true"
launcher:iconDisplay="shortcut_popup" launcher:iconDisplay="shortcut_popup"
launcher:iconSizeOverride="@dimen/deep_shortcut_icon_size" /> launcher:iconSizeOverride="@dimen/deep_shortcut_icon_size" />

Some files were not shown because too many files have changed in this diff Show More