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:
+3
-2
@@ -251,7 +251,7 @@ LOCAL_PROGUARD_ENABLED := full
|
||||
LOCAL_PACKAGE_NAME := Launcher3QuickStepGo
|
||||
LOCAL_PRIVILEGED_MODULE := true
|
||||
LOCAL_PRODUCT_MODULE := true
|
||||
LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
|
||||
LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep Launcher3GoIconRecents
|
||||
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
|
||||
|
||||
LOCAL_FULL_LIBS_MANIFEST_FILES := \
|
||||
@@ -297,7 +297,8 @@ LOCAL_PROGUARD_ENABLED := full
|
||||
|
||||
LOCAL_PACKAGE_NAME := Launcher3GoIconRecents
|
||||
LOCAL_PRIVILEGED_MODULE := true
|
||||
LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
|
||||
LOCAL_PRODUCT_MODULE := true
|
||||
LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep Launcher3QuickStepGo
|
||||
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
|
||||
|
||||
LOCAL_FULL_LIBS_MANIFEST_FILES := \
|
||||
|
||||
@@ -14,4 +14,9 @@
|
||||
See the License for the specific language governing permissions and
|
||||
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>
|
||||
+6
-6
@@ -14,10 +14,10 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<com.android.quickstep.hints.ChipsContainer
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/chip_hint_height"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="horizontal"/>
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
<stroke android:color="@android:color/white" android:width="4px"/>
|
||||
<corners android:radius="2dp"/>
|
||||
</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_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:id="@+id/recent_task_content_view"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recent_task_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
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>
|
||||
android:scrollbars="none"/>
|
||||
<TextView
|
||||
android:id="@+id/recent_task_empty_view"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -17,26 +17,23 @@
|
||||
<com.android.quickstep.views.TaskItemView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
<FrameLayout
|
||||
<com.android.quickstep.views.TaskThumbnailIconView
|
||||
android:id="@+id/task_icon_and_thumbnail"
|
||||
android:layout_width="@dimen/task_thumbnail_and_icon_view_size"
|
||||
android:layout_height="@dimen/task_thumbnail_and_icon_view_size"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginVertical="@dimen/task_item_half_vert_margin">
|
||||
android:layout_marginTop="16dp">
|
||||
<ImageView
|
||||
android:id="@+id/task_thumbnail"
|
||||
android:layout_width="@dimen/task_thumbnail_width"
|
||||
android:layout_height="@dimen/task_thumbnail_height"
|
||||
android:layout_gravity="top|start"/>
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
<ImageView
|
||||
android:id="@+id/task_icon"
|
||||
android:layout_width="@dimen/task_icon_size"
|
||||
android:layout_height="@dimen/task_icon_size"
|
||||
android:layout_gravity="bottom|end"/>
|
||||
</FrameLayout>
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
</com.android.quickstep.views.TaskThumbnailIconView>
|
||||
<TextView
|
||||
android:id="@+id/task_label"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -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.anim.Interpolators.DEACCEL_2;
|
||||
import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
|
||||
|
||||
import com.android.launcher3.DeviceProfile;
|
||||
import com.android.launcher3.Launcher;
|
||||
@@ -51,6 +52,9 @@ public class OverviewState extends LauncherState {
|
||||
public void onStateEnabled(Launcher launcher) {
|
||||
IconRecentsView recentsView = launcher.getOverviewPanel();
|
||||
recentsView.onBeginTransitionToOverview();
|
||||
// Request orientation be set to unspecified, letting the system decide the best
|
||||
// orientation.
|
||||
launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -131,10 +131,11 @@ final class AppToOverviewAnimationProvider<T extends BaseDraggingActivity> imple
|
||||
return anim;
|
||||
}
|
||||
|
||||
View thumbnailView = mRecentsView.getThumbnailViewForTask(mTargetTaskId);
|
||||
View thumbnailView = mRecentsView.getBottomThumbnailView();
|
||||
if (thumbnailView == null) {
|
||||
// TODO: We should either 1) guarantee the view is loaded before attempting this
|
||||
// or 2) have a backup animation.
|
||||
// This can be null if there were previously 0 tasks and the recycler view has not had
|
||||
// enough time to take in the data change, bind a new view, and lay out the new view.
|
||||
// TODO: Have a fallback to animate to
|
||||
if (Log.isLoggable(TAG, Log.WARN)) {
|
||||
Log.w(TAG, "No thumbnail view for running task. Using stub animation.");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.quickstep.views.TaskItemView;
|
||||
import com.android.systemui.shared.recents.model.Task;
|
||||
import com.android.systemui.shared.system.ActivityManagerWrapper;
|
||||
@@ -37,12 +41,12 @@ public final class TaskActionController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the task associated with the task holder, animating into the app.
|
||||
* Launch the task associated with the task holder, animating into the app from the task view.
|
||||
*
|
||||
* @param viewHolder the task view holder to launch
|
||||
*/
|
||||
public void launchTask(TaskHolder viewHolder) {
|
||||
if (viewHolder.getTask() == null) {
|
||||
public void launchTaskFromView(@NonNull TaskHolder viewHolder) {
|
||||
if (!viewHolder.getTask().isPresent()) {
|
||||
return;
|
||||
}
|
||||
TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
|
||||
@@ -53,8 +57,20 @@ public final class TaskActionController {
|
||||
int height = v.getMeasuredHeight();
|
||||
|
||||
ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
|
||||
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(viewHolder.getTask().key,
|
||||
opts, null /* resultCallback */, null /* resultCallbackHandler */);
|
||||
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(
|
||||
viewHolder.getTask().get().key, opts, null /* resultCallback */,
|
||||
null /* resultCallbackHandler */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the task directly with a basic animation.
|
||||
*
|
||||
* @param task the task to launch
|
||||
*/
|
||||
public void launchTask(@NonNull Task task) {
|
||||
ActivityOptions opts = ActivityOptions.makeBasic();
|
||||
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(task.key, opts,
|
||||
null /* resultCallback */, null /* resultCallbackHandler */);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,11 +79,11 @@ public final class TaskActionController {
|
||||
* @param viewHolder the task view holder to remove
|
||||
*/
|
||||
public void removeTask(TaskHolder viewHolder) {
|
||||
if (viewHolder.getTask() == null) {
|
||||
if (!viewHolder.getTask().isPresent()) {
|
||||
return;
|
||||
}
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
Task task = viewHolder.getTask();
|
||||
Task task = viewHolder.getTask().get();
|
||||
ActivityManagerWrapper.getInstance().removeTask(task.key.id);
|
||||
mLoader.removeTask(task);
|
||||
mAdapter.notifyItemRemoved(position);
|
||||
@@ -80,6 +96,6 @@ public final class TaskActionController {
|
||||
int count = mAdapter.getItemCount();
|
||||
ActivityManagerWrapper.getInstance().removeAllRecentTasks();
|
||||
mLoader.clearAllTasks();
|
||||
mAdapter.notifyItemRangeRemoved(0 /* positionStart */, count);
|
||||
mAdapter.notifyItemRangeRemoved(TASKS_START_POSITION /* positionStart */, count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
*/
|
||||
package com.android.quickstep;
|
||||
|
||||
import android.util.ArrayMap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
import com.android.quickstep.views.TaskItemView;
|
||||
@@ -29,18 +31,25 @@ import com.android.systemui.shared.recents.model.Task;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the
|
||||
* appropriate {@link Task} from the recents task list.
|
||||
*/
|
||||
public final class TaskAdapter extends Adapter<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 final TaskListLoader mLoader;
|
||||
private final ArrayMap<Integer, TaskItemView> mTaskIdToViewMap = new ArrayMap<>();
|
||||
private TaskActionController mTaskActionController;
|
||||
private OnClickListener mClearAllListener;
|
||||
private boolean mIsShowingLoadingUi;
|
||||
|
||||
public TaskAdapter(@NonNull TaskListLoader loader) {
|
||||
@@ -51,6 +60,10 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
|
||||
mTaskActionController = taskActionController;
|
||||
}
|
||||
|
||||
public void setOnClearAllClickListener(OnClickListener listener) {
|
||||
mClearAllListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all positions in the task adapter to loading views, binding new views if necessary.
|
||||
* This changes the task adapter's view of the data, so the appropriate notify events should be
|
||||
@@ -63,75 +76,104 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
|
||||
mIsShowingLoadingUi = isShowingLoadingUi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task item view for a given task id if it's attached to the view.
|
||||
*
|
||||
* @param taskId task id to search for
|
||||
* @return corresponding task item view if it's attached, null otherwise
|
||||
*/
|
||||
public @Nullable TaskItemView getTaskItemView(int taskId) {
|
||||
return mTaskIdToViewMap.get(taskId);
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case ITEM_TYPE_TASK:
|
||||
TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.task_item_view, parent, false);
|
||||
TaskHolder taskHolder = new TaskHolder(itemView);
|
||||
itemView.setOnClickListener(
|
||||
view -> mTaskActionController.launchTaskFromView(taskHolder));
|
||||
return taskHolder;
|
||||
case ITEM_TYPE_CLEAR_ALL:
|
||||
View clearView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.clear_all_button, parent, false);
|
||||
ClearAllHolder clearAllHolder = new ClearAllHolder(clearView);
|
||||
Button clearViewButton = clearView.findViewById(R.id.clear_all_button);
|
||||
clearViewButton.setOnClickListener(mClearAllListener);
|
||||
return clearAllHolder;
|
||||
default:
|
||||
throw new IllegalArgumentException("No known holder for item type: " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.task_item_view, parent, false);
|
||||
TaskHolder holder = new TaskHolder(itemView);
|
||||
itemView.setOnClickListener(view -> mTaskActionController.launchTask(holder));
|
||||
return holder;
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
onBindViewHolderInternal(holder, position, false /* willAnimate */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TaskHolder holder, int position) {
|
||||
if (mIsShowingLoadingUi) {
|
||||
holder.bindEmptyUi();
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position,
|
||||
@NonNull List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
super.onBindViewHolder(holder, position, payloads);
|
||||
return;
|
||||
}
|
||||
List<Task> tasks = mLoader.getCurrentTaskList();
|
||||
if (position >= tasks.size()) {
|
||||
// Task list has updated.
|
||||
return;
|
||||
int changeType = (int) payloads.get(0);
|
||||
if (changeType == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
|
||||
// Bind in preparation for animation
|
||||
onBindViewHolderInternal(holder, position, true /* willAnimate */);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Payload content is not a valid change event type: "
|
||||
+ changeType);
|
||||
}
|
||||
}
|
||||
|
||||
private void onBindViewHolderInternal(@NonNull ViewHolder holder, int position,
|
||||
boolean willAnimate) {
|
||||
int itemType = getItemViewType(position);
|
||||
switch (itemType) {
|
||||
case ITEM_TYPE_TASK:
|
||||
TaskHolder taskHolder = (TaskHolder) holder;
|
||||
if (mIsShowingLoadingUi) {
|
||||
taskHolder.bindEmptyUi();
|
||||
return;
|
||||
}
|
||||
List<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
|
||||
public void onViewAttachedToWindow(@NonNull TaskHolder holder) {
|
||||
if (holder.getTask() == null) {
|
||||
return;
|
||||
}
|
||||
mTaskIdToViewMap.put(holder.getTask().key.id, (TaskItemView) holder.itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull TaskHolder holder) {
|
||||
if (holder.getTask() == null) {
|
||||
return;
|
||||
}
|
||||
mTaskIdToViewMap.remove(holder.getTask().key.id);
|
||||
public int getItemViewType(int position) {
|
||||
// Bottom is always clear all button.
|
||||
return (position == 0) ? ITEM_TYPE_CLEAR_ALL : ITEM_TYPE_TASK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int itemCount = TASKS_START_POSITION;
|
||||
if (mIsShowingLoadingUi) {
|
||||
// Show loading version of all items.
|
||||
return MAX_TASKS_TO_DISPLAY;
|
||||
itemCount += MAX_TASKS_TO_DISPLAY;
|
||||
} else {
|
||||
return Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
|
||||
itemCount += Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
|
||||
}
|
||||
return itemCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
*/
|
||||
package com.android.quickstep;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import com.android.quickstep.views.TaskItemView;
|
||||
import com.android.systemui.shared.recents.model.Task;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A recycler view holder that holds the task view and binds {@link Task} content (app title, icon,
|
||||
* etc.) to the view.
|
||||
@@ -40,13 +42,27 @@ public final class TaskHolder extends ViewHolder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a task to the holder, resetting the view and preparing it for content to load in.
|
||||
* Bind the task model to the holder. This will take the current task content in the task
|
||||
* object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
|
||||
* the content to animate to at a later time. If the task does not have all its content loaded,
|
||||
* the view will prepare appropriate default placeholders and it is the callers responsibility
|
||||
* to change them at a later time.
|
||||
*
|
||||
* Regardless of whether it is animating, input handlers will be bound immediately (see
|
||||
* {@link TaskActionController}).
|
||||
*
|
||||
* @param task the task to bind to the view
|
||||
* @param willAnimate true if UI should animate in later, false if it should apply immediately
|
||||
*/
|
||||
public void bindTask(Task task) {
|
||||
public void bindTask(@NonNull Task task, boolean willAnimate) {
|
||||
mTask = task;
|
||||
mTaskItemView.resetTaskItemView();
|
||||
if (willAnimate) {
|
||||
mTaskItemView.startContentAnimation(task.icon, task.thumbnail, task.titleDescription);
|
||||
} else {
|
||||
mTaskItemView.setIcon(task.icon);
|
||||
mTaskItemView.setThumbnail(task.thumbnail);
|
||||
mTaskItemView.setLabel(task.titleDescription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,10 +71,7 @@ public final class TaskHolder extends ViewHolder {
|
||||
*/
|
||||
public void bindEmptyUi() {
|
||||
mTask = null;
|
||||
// TODO: Set the task view to a loading, empty UI.
|
||||
// Temporarily using the one below for visual confirmation but should be swapped out to new
|
||||
// UI later.
|
||||
mTaskItemView.resetTaskItemView();
|
||||
mTaskItemView.resetToEmptyUi();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +79,7 @@ public final class TaskHolder extends ViewHolder {
|
||||
*
|
||||
* @return the current task
|
||||
*/
|
||||
public @Nullable Task getTask() {
|
||||
return mTask;
|
||||
public Optional<Task> getTask() {
|
||||
return Optional.ofNullable(mTask);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,23 +38,9 @@ public final class TaskListLoader {
|
||||
|
||||
private ArrayList<Task> mTaskList = new ArrayList<>();
|
||||
private int mTaskListChangeId;
|
||||
private RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
|
||||
Task foundTask = null;
|
||||
for (Task task : mTaskList) {
|
||||
if (task.key.id == taskId) {
|
||||
foundTask = task;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundTask != null) {
|
||||
foundTask.thumbnail = thumbnailData;
|
||||
}
|
||||
return foundTask;
|
||||
};
|
||||
|
||||
public TaskListLoader(Context context) {
|
||||
mRecentsModel = RecentsModel.INSTANCE.get(context);
|
||||
mRecentsModel.addThumbnailChangeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,9 @@ package com.android.quickstep;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
|
||||
|
||||
import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
@@ -45,4 +48,14 @@ public final class TaskSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
mTaskActionController.removeTask((TaskHolder) viewHolder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSwipeDirs(@NonNull RecyclerView recyclerView,
|
||||
@NonNull ViewHolder viewHolder) {
|
||||
if (viewHolder.getItemViewType() == ITEM_TYPE_CLEAR_ALL) {
|
||||
// Clear all button should not be swipable.
|
||||
return 0;
|
||||
}
|
||||
return super.getSwipeDirs(recyclerView, viewHolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
|
||||
|
||||
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
|
||||
|
||||
import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
|
||||
import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
@@ -24,30 +29,39 @@ import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.util.ArraySet;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.FloatProperty;
|
||||
import android.view.View;
|
||||
import android.view.ViewDebug;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.view.animation.LayoutAnimationController;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
|
||||
|
||||
import com.android.launcher3.BaseActivity;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.quickstep.ContentFillItemAnimator;
|
||||
import com.android.quickstep.RecentsModel;
|
||||
import com.android.quickstep.RecentsToActivityHelper;
|
||||
import com.android.quickstep.TaskActionController;
|
||||
import com.android.quickstep.TaskAdapter;
|
||||
import com.android.quickstep.TaskHolder;
|
||||
import com.android.quickstep.TaskListLoader;
|
||||
import com.android.quickstep.TaskSwipeCallback;
|
||||
import com.android.systemui.shared.recents.model.Task;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
|
||||
@@ -89,24 +103,48 @@ public final class IconRecentsView extends FrameLayout {
|
||||
private final Context mContext;
|
||||
private final TaskListLoader mTaskLoader;
|
||||
private final TaskAdapter mTaskAdapter;
|
||||
private final LinearLayoutManager mTaskLayoutManager;
|
||||
private final TaskActionController mTaskActionController;
|
||||
private final LayoutAnimationController mLayoutAnimation;
|
||||
private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
|
||||
private final ContentFillItemAnimator mLoadingContentItemAnimator =
|
||||
new ContentFillItemAnimator();
|
||||
|
||||
private RecentsToActivityHelper mActivityHelper;
|
||||
private RecyclerView mTaskRecyclerView;
|
||||
private View mShowingContentView;
|
||||
private View mEmptyView;
|
||||
private View mContentView;
|
||||
private View mClearAllView;
|
||||
private boolean mTransitionedFromApp;
|
||||
private AnimatorSet mLayoutAnimation;
|
||||
private final ArraySet<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) {
|
||||
super(context, attrs);
|
||||
BaseActivity activity = BaseActivity.fromContext(context);
|
||||
mContext = context;
|
||||
mTaskLoader = new TaskListLoader(mContext);
|
||||
mTaskAdapter = new TaskAdapter(mTaskLoader);
|
||||
mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
|
||||
mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
|
||||
mTaskAdapter.setActionController(mTaskActionController);
|
||||
mLayoutAnimation = createLayoutAnimation();
|
||||
mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
|
||||
RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -115,15 +153,30 @@ public final class IconRecentsView extends FrameLayout {
|
||||
if (mTaskRecyclerView == null) {
|
||||
mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
|
||||
mTaskRecyclerView.setAdapter(mTaskAdapter);
|
||||
mTaskRecyclerView.setLayoutManager(
|
||||
new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */));
|
||||
mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
|
||||
ItemTouchHelper helper = new ItemTouchHelper(
|
||||
new TaskSwipeCallback(mTaskActionController));
|
||||
helper.attachToRecyclerView(mTaskRecyclerView);
|
||||
mTaskRecyclerView.setLayoutAnimation(mLayoutAnimation);
|
||||
mTaskRecyclerView.addOnChildAttachStateChangeListener(
|
||||
new OnChildAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(@NonNull View view) {
|
||||
if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
|
||||
// Child view was added that is not part of current layout animation
|
||||
// so restart the animation.
|
||||
animateFadeInLayoutAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(@NonNull View view) { }
|
||||
});
|
||||
mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
|
||||
mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
|
||||
() -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
|
||||
|
||||
mEmptyView = findViewById(R.id.recent_task_empty_view);
|
||||
mContentView = findViewById(R.id.recent_task_content_view);
|
||||
mContentView = mTaskRecyclerView;
|
||||
mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
@@ -135,19 +188,17 @@ public final class IconRecentsView extends FrameLayout {
|
||||
updateContentViewVisibility();
|
||||
}
|
||||
});
|
||||
mClearAllView = findViewById(R.id.clear_all_button);
|
||||
mClearAllView.setOnClickListener(v -> animateClearAllTasks());
|
||||
// TODO: Move layout param logic into onMeasure
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
TaskItemView[] itemViews = getTaskViews();
|
||||
for (TaskItemView itemView : itemViews) {
|
||||
itemView.setEnabled(enabled);
|
||||
int childCount = mTaskRecyclerView.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
|
||||
}
|
||||
mClearAllView.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,8 +216,13 @@ public final class IconRecentsView extends FrameLayout {
|
||||
* becomes visible.
|
||||
*/
|
||||
public void onBeginTransitionToOverview() {
|
||||
mTaskRecyclerView.scheduleLayoutAnimation();
|
||||
|
||||
if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
|
||||
// Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
|
||||
// all tasks should be visible to fill up the screen in portrait mode and the view will
|
||||
// not be scrollable.
|
||||
mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
|
||||
}
|
||||
scheduleFadeInLayoutAnimation();
|
||||
// Load any task changes
|
||||
if (!mTaskLoader.needsToLoad()) {
|
||||
return;
|
||||
@@ -174,9 +230,24 @@ public final class IconRecentsView extends FrameLayout {
|
||||
mTaskAdapter.setIsShowingLoadingUi(true);
|
||||
mTaskAdapter.notifyDataSetChanged();
|
||||
mTaskLoader.loadTaskList(tasks -> {
|
||||
int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
|
||||
mTaskAdapter.setIsShowingLoadingUi(false);
|
||||
// TODO: Animate the loading UI out and the loaded data in.
|
||||
mTaskAdapter.notifyDataSetChanged();
|
||||
int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
|
||||
if (numEmptyItems < numActualItems) {
|
||||
throw new IllegalStateException("There are less empty item views than the number "
|
||||
+ "of items to animate to.");
|
||||
}
|
||||
// Possible that task list loads faster than adapter changes propagate to layout so
|
||||
// only start content fill animation if there aren't any pending adapter changes.
|
||||
if (!mTaskRecyclerView.hasPendingAdapterUpdates()) {
|
||||
// Set item animator for content filling animation. The item animator will switch
|
||||
// back to the default on completion
|
||||
mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
|
||||
}
|
||||
mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
|
||||
numEmptyItems - numActualItems);
|
||||
mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
|
||||
CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -194,35 +265,47 @@ public final class IconRecentsView extends FrameLayout {
|
||||
* the app. In that case, we launch the next most recent.
|
||||
*/
|
||||
public void handleOverviewCommand() {
|
||||
int childCount = mTaskRecyclerView.getChildCount();
|
||||
if (childCount == 0) {
|
||||
List<Task> tasks = mTaskLoader.getCurrentTaskList();
|
||||
int tasksSize = tasks.size();
|
||||
if (tasksSize == 0) {
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
TaskHolder taskToLaunch;
|
||||
if (mTransitionedFromApp && childCount > 1) {
|
||||
Task taskToLaunch;
|
||||
if (mTransitionedFromApp && tasksSize > 1) {
|
||||
// Launch the next most recent app
|
||||
TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(1);
|
||||
taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
|
||||
taskToLaunch = tasks.get(1);
|
||||
} else {
|
||||
// Launch the most recent app
|
||||
TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(0);
|
||||
taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
|
||||
taskToLaunch = tasks.get(0);
|
||||
}
|
||||
|
||||
// See if view for this task is attached, and if so, animate launch from that view.
|
||||
ArrayList<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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thumbnail view associated with a task for the purposes of animation.
|
||||
* Get the bottom most thumbnail view to animate to.
|
||||
*
|
||||
* @param taskId task id of thumbnail view to get
|
||||
* @return the thumbnail view for the task if attached, null otherwise
|
||||
* @return the thumbnail view if laid out
|
||||
*/
|
||||
public @Nullable View getThumbnailViewForTask(int taskId) {
|
||||
TaskItemView view = mTaskAdapter.getTaskItemView(taskId);
|
||||
if (view == null) {
|
||||
public @Nullable View getBottomThumbnailView() {
|
||||
ArrayList<TaskItemView> taskViews = getTaskViews();
|
||||
if (taskViews.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
TaskItemView view = taskViews.get(0);
|
||||
return view.getThumbnailView();
|
||||
}
|
||||
|
||||
@@ -231,13 +314,14 @@ public final class IconRecentsView extends FrameLayout {
|
||||
*/
|
||||
private void animateClearAllTasks() {
|
||||
setEnabled(false);
|
||||
TaskItemView[] itemViews = getTaskViews();
|
||||
ArrayList<TaskItemView> itemViews = getTaskViews();
|
||||
|
||||
AnimatorSet clearAnim = new AnimatorSet();
|
||||
long currentDelay = 0;
|
||||
|
||||
// Animate each item view to the right and fade out.
|
||||
for (TaskItemView itemView : itemViews) {
|
||||
for (int i = 0, size = itemViews.size(); i < size; i++) {
|
||||
TaskItemView itemView = itemViews.get(i);
|
||||
PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
|
||||
0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
|
||||
PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
|
||||
@@ -272,7 +356,8 @@ public final class IconRecentsView extends FrameLayout {
|
||||
clearAnim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
for (TaskItemView itemView : itemViews) {
|
||||
for (int i = 0, size = itemViews.size(); i < size; i++) {
|
||||
TaskItemView itemView = itemViews.get(i);
|
||||
itemView.setTranslationX(0);
|
||||
itemView.setAlpha(1.0f);
|
||||
}
|
||||
@@ -287,13 +372,16 @@ public final class IconRecentsView extends FrameLayout {
|
||||
/**
|
||||
* Get attached task item views ordered by most recent.
|
||||
*
|
||||
* @return array of attached task item views
|
||||
* @return array list of attached task item views
|
||||
*/
|
||||
private TaskItemView[] getTaskViews() {
|
||||
private ArrayList<TaskItemView> getTaskViews() {
|
||||
int taskCount = mTaskRecyclerView.getChildCount();
|
||||
TaskItemView[] itemViews = new TaskItemView[taskCount];
|
||||
ArrayList<TaskItemView> itemViews = new ArrayList<>();
|
||||
for (int i = 0; i < taskCount; i ++) {
|
||||
itemViews[i] = (TaskItemView) mTaskRecyclerView.getChildAt(i);
|
||||
View child = mTaskRecyclerView.getChildAt(i);
|
||||
if (child instanceof TaskItemView) {
|
||||
itemViews.add((TaskItemView) child);
|
||||
}
|
||||
}
|
||||
return itemViews;
|
||||
}
|
||||
@@ -303,12 +391,14 @@ public final class IconRecentsView extends FrameLayout {
|
||||
* of tasks.
|
||||
*/
|
||||
private void updateContentViewVisibility() {
|
||||
int taskListSize = mTaskLoader.getCurrentTaskList().size();
|
||||
if (mEmptyView.getVisibility() != VISIBLE && taskListSize == 0) {
|
||||
int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
|
||||
if (mShowingContentView != mEmptyView && taskListSize == 0) {
|
||||
mShowingContentView = mEmptyView;
|
||||
crossfadeViews(mEmptyView, mContentView);
|
||||
mActivityHelper.leaveRecents();
|
||||
}
|
||||
if (mContentView.getVisibility() != VISIBLE && taskListSize > 0) {
|
||||
if (mShowingContentView != mContentView && taskListSize > 0) {
|
||||
mShowingContentView = mContentView;
|
||||
crossfadeViews(mContentView, mEmptyView);
|
||||
}
|
||||
}
|
||||
@@ -320,6 +410,7 @@ public final class IconRecentsView extends FrameLayout {
|
||||
* @param fadeOutView view that should fade out
|
||||
*/
|
||||
private void crossfadeViews(View fadeInView, View fadeOutView) {
|
||||
fadeInView.animate().cancel();
|
||||
fadeInView.setVisibility(VISIBLE);
|
||||
fadeInView.setAlpha(0f);
|
||||
fadeInView.animate()
|
||||
@@ -327,6 +418,7 @@ public final class IconRecentsView extends FrameLayout {
|
||||
.setDuration(CROSSFADE_DURATION)
|
||||
.setListener(null);
|
||||
|
||||
fadeOutView.animate().cancel();
|
||||
fadeOutView.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(CROSSFADE_DURATION)
|
||||
@@ -338,17 +430,56 @@ public final class IconRecentsView extends FrameLayout {
|
||||
});
|
||||
}
|
||||
|
||||
private static LayoutAnimationController createLayoutAnimation() {
|
||||
AnimationSet anim = new AnimationSet(false /* shareInterpolator */);
|
||||
/**
|
||||
* Schedule a one-shot layout animation on the next layout. Separate from
|
||||
* {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
|
||||
* view properties themselves, allowing more controllable behavior and making it easier to
|
||||
* manage when the animation conflicts with another animation.
|
||||
*/
|
||||
private void scheduleFadeInLayoutAnimation() {
|
||||
mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
animateFadeInLayoutAnimation();
|
||||
mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Animation alphaAnim = new AlphaAnimation(0, 1);
|
||||
alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION);
|
||||
anim.addAnimation(alphaAnim);
|
||||
|
||||
LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
|
||||
layoutAnim.setDelay(
|
||||
(float) LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN / LAYOUT_ITEM_ANIMATE_IN_DURATION);
|
||||
|
||||
return layoutAnim;
|
||||
/**
|
||||
* Start animating the layout animation where items fade in.
|
||||
*/
|
||||
private void animateFadeInLayoutAnimation() {
|
||||
if (mLayoutAnimation != null) {
|
||||
// If layout animation still in progress, cancel and restart.
|
||||
mLayoutAnimation.cancel();
|
||||
}
|
||||
ArrayList<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;
|
||||
|
||||
import static com.android.quickstep.views.TaskLayoutUtils.getTaskHeight;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.FloatProperty;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
import com.android.quickstep.ThumbnailDrawable;
|
||||
import com.android.systemui.shared.recents.model.ThumbnailData;
|
||||
|
||||
/**
|
||||
* View representing an individual task item with the icon + thumbnail adjacent to the task label.
|
||||
*/
|
||||
public final class TaskItemView extends LinearLayout {
|
||||
|
||||
private static final String EMPTY_LABEL = "";
|
||||
private static final String DEFAULT_LABEL = "...";
|
||||
private final Drawable mDefaultIcon;
|
||||
private final Drawable mDefaultThumbnail;
|
||||
private final TaskLayerDrawable mIconDrawable;
|
||||
private final TaskLayerDrawable mThumbnailDrawable;
|
||||
private TextView mLabelView;
|
||||
private ImageView mIconView;
|
||||
private ImageView mThumbnailView;
|
||||
private float mContentTransitionProgress;
|
||||
|
||||
/**
|
||||
* Property representing the content transition progress of the view. 1.0f represents that the
|
||||
* currently bound icon, thumbnail, and label are fully animated in and visible.
|
||||
*/
|
||||
public static FloatProperty CONTENT_TRANSITION_PROGRESS =
|
||||
new FloatProperty<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) {
|
||||
super(context, attrs);
|
||||
mDefaultIcon = context.getResources().getDrawable(
|
||||
android.R.drawable.sym_def_app_icon, context.getTheme());
|
||||
Resources res = context.getResources();
|
||||
mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
|
||||
mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
|
||||
mIconDrawable = new TaskLayerDrawable(context);
|
||||
mThumbnailDrawable = new TaskLayerDrawable(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,15 +83,28 @@ public final class TaskItemView extends LinearLayout {
|
||||
mLabelView = findViewById(R.id.task_label);
|
||||
mThumbnailView = findViewById(R.id.task_thumbnail);
|
||||
mIconView = findViewById(R.id.task_icon);
|
||||
|
||||
mThumbnailView.setImageDrawable(mThumbnailDrawable);
|
||||
mIconView.setImageDrawable(mIconDrawable);
|
||||
|
||||
resetToEmptyUi();
|
||||
CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int taskHeight = getTaskHeight(getContext());
|
||||
int newHeightSpec = MeasureSpec.makeMeasureSpec(taskHeight,MeasureSpec.EXACTLY);
|
||||
super.onMeasure(widthMeasureSpec, newHeightSpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets task item view to default values.
|
||||
* Resets task item view to empty, loading UI.
|
||||
*/
|
||||
public void resetTaskItemView() {
|
||||
setLabel(DEFAULT_LABEL);
|
||||
setIcon(null);
|
||||
setThumbnail(null);
|
||||
public void resetToEmptyUi() {
|
||||
mIconDrawable.resetDrawable();
|
||||
mThumbnailDrawable.resetDrawable();
|
||||
setLabel(EMPTY_LABEL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,11 +113,8 @@ public final class TaskItemView extends LinearLayout {
|
||||
* @param label task label
|
||||
*/
|
||||
public void setLabel(@Nullable String label) {
|
||||
if (label == null) {
|
||||
mLabelView.setText(DEFAULT_LABEL);
|
||||
return;
|
||||
}
|
||||
mLabelView.setText(label);
|
||||
mLabelView.setText(getSafeLabel(label));
|
||||
// TODO: Animation for label
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,29 +127,72 @@ public final class TaskItemView extends LinearLayout {
|
||||
// The icon proper is actually smaller than the drawable and has "padding" on the side for
|
||||
// the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
|
||||
// view if we want the icon to be flush with the bottom of the thumbnail.
|
||||
if (icon == null) {
|
||||
mIconView.setImageDrawable(mDefaultIcon);
|
||||
return;
|
||||
}
|
||||
mIconView.setImageDrawable(icon);
|
||||
mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the task thumbnail for the task. Sets to a default thumbnail if null.
|
||||
*
|
||||
* @param thumbnail task thumbnail for the task
|
||||
* @param thumbnailData task thumbnail data for the task
|
||||
*/
|
||||
public void setThumbnail(@Nullable Bitmap thumbnail) {
|
||||
if (thumbnail == null) {
|
||||
mThumbnailView.setImageBitmap(null);
|
||||
mThumbnailView.setBackgroundColor(Color.GRAY);
|
||||
return;
|
||||
}
|
||||
mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
|
||||
mThumbnailView.setImageBitmap(thumbnail);
|
||||
public void setThumbnail(@Nullable ThumbnailData thumbnailData) {
|
||||
mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnailData));
|
||||
}
|
||||
|
||||
public View getThumbnailView() {
|
||||
return mThumbnailView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new animation from the current task content to the specified new content. The caller
|
||||
* is responsible for the actual animation control via the property
|
||||
* {@link #CONTENT_TRANSITION_PROGRESS}.
|
||||
*
|
||||
* @param endIcon the icon to animate to
|
||||
* @param endThumbnail the thumbnail to animate to
|
||||
* @param endLabel the label to animate to
|
||||
*/
|
||||
public void startContentAnimation(@Nullable Drawable endIcon,
|
||||
@Nullable ThumbnailData endThumbnail, @Nullable String endLabel) {
|
||||
mIconDrawable.startNewTransition(getSafeIcon(endIcon));
|
||||
mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
|
||||
// TODO: Animation for label
|
||||
|
||||
setContentTransitionProgress(0.0f);
|
||||
}
|
||||
|
||||
private void setContentTransitionProgress(float progress) {
|
||||
mContentTransitionProgress = progress;
|
||||
mIconDrawable.setTransitionProgress(progress);
|
||||
mThumbnailDrawable.setTransitionProgress(progress);
|
||||
// TODO: Animation for label
|
||||
}
|
||||
|
||||
private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
|
||||
return (icon != null) ? icon : mDefaultIcon;
|
||||
}
|
||||
|
||||
private @NonNull Drawable getSafeThumbnail(@Nullable ThumbnailData thumbnailData) {
|
||||
if (thumbnailData == null || thumbnailData.thumbnail == null) {
|
||||
return mDefaultThumbnail;
|
||||
}
|
||||
int orientation = getResources().getConfiguration().orientation;
|
||||
return new ThumbnailDrawable(thumbnailData, orientation /* requestedOrientation */);
|
||||
}
|
||||
|
||||
private @NonNull String getSafeLabel(@Nullable String label) {
|
||||
return (label != null) ? label : DEFAULT_LABEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
int layerCount = mThumbnailDrawable.getNumberOfLayers();
|
||||
for (int i = 0; i < layerCount; i++) {
|
||||
Drawable drawable = mThumbnailDrawable.getDrawable(i);
|
||||
if (drawable instanceof ThumbnailDrawable) {
|
||||
((ThumbnailDrawable) drawable).setRequestedOrientation(newConfig.orientation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Context;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
@@ -55,7 +56,7 @@ public class DeepShortcutManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onShortcutsChanged(List<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,
|
||||
* 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) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -73,7 +74,7 @@ public class DeepShortcutManager {
|
||||
* Gets all the manifest and dynamic shortcuts associated with the given package and user,
|
||||
* to be displayed in the shortcuts container on long press.
|
||||
*/
|
||||
public List<ShortcutInfoCompat> queryForShortcutsContainer(ComponentName activity,
|
||||
public List<ShortcutInfo> queryForShortcutsContainer(ComponentName activity,
|
||||
UserHandle user) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -96,7 +97,7 @@ public class DeepShortcutManager {
|
||||
Bundle startActivityOptions, UserHandle user) {
|
||||
}
|
||||
|
||||
public Drawable getShortcutIconDrawable(ShortcutInfoCompat shortcutInfo, int density) {
|
||||
public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -105,16 +106,16 @@ public class DeepShortcutManager {
|
||||
*
|
||||
* If packageName is null, returns all pinned shortcuts regardless of package.
|
||||
*/
|
||||
public List<ShortcutInfoCompat> queryForPinnedShortcuts(String packageName, UserHandle user) {
|
||||
public List<ShortcutInfo> queryForPinnedShortcuts(String packageName, UserHandle user) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public List<ShortcutInfoCompat> queryForPinnedShortcuts(String packageName,
|
||||
public List<ShortcutInfo> queryForPinnedShortcuts(String packageName,
|
||||
List<String> shortcutIds, UserHandle user) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public List<ShortcutInfoCompat> queryForAllShortcuts(UserHandle user) {
|
||||
public List<ShortcutInfo> queryForAllShortcuts(UserHandle user) {
|
||||
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"?>
|
||||
<resources>
|
||||
<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>
|
||||
@@ -12,4 +12,13 @@
|
||||
<dimen name="chip_text_top_padding">4dp</dimen>
|
||||
<dimen name="chip_text_start_padding">10dp</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>
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
<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>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
+207
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+308
@@ -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;
|
||||
}
|
||||
}
|
||||
+69
@@ -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;
|
||||
}
|
||||
}
|
||||
+242
@@ -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);
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
}
|
||||
+214
@@ -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();
|
||||
}
|
||||
}
|
||||
+411
@@ -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;
|
||||
}
|
||||
}
|
||||
+330
@@ -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
-7
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.android.launcher3.uioverrides;
|
||||
|
||||
import static com.android.launcher3.LauncherState.RECENTS_CLEAR_ALL_BUTTON;
|
||||
import static com.android.launcher3.anim.Interpolators.LINEAR;
|
||||
import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
@@ -22,15 +24,17 @@ import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.util.FloatProperty;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.LauncherState;
|
||||
import com.android.launcher3.LauncherStateManager.AnimationConfig;
|
||||
import com.android.launcher3.anim.AnimatorSetBuilder;
|
||||
import com.android.launcher3.anim.PropertySetter;
|
||||
import com.android.quickstep.views.ClearAllButton;
|
||||
import com.android.quickstep.views.LauncherRecentsView;
|
||||
import com.android.quickstep.views.RecentsView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing
|
||||
* the basic view properties, this class also manages changes in the task visuals.
|
||||
@@ -49,10 +53,8 @@ public final class RecentsViewStateController extends
|
||||
if (state.overviewUi) {
|
||||
mRecentsView.updateEmptyMessage();
|
||||
mRecentsView.resetTaskVisuals();
|
||||
mRecentsView.setHintVisibility(1);
|
||||
} else {
|
||||
mRecentsView.setHintVisibility(0);
|
||||
}
|
||||
setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state.getVisibleElements(mLauncher));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,7 +64,6 @@ public final class RecentsViewStateController extends
|
||||
|
||||
if (!toState.overviewUi) {
|
||||
builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals);
|
||||
mRecentsView.setHintVisibility(0);
|
||||
}
|
||||
|
||||
if (toState.overviewUi) {
|
||||
@@ -74,8 +75,15 @@ public final class RecentsViewStateController extends
|
||||
updateAnim.setDuration(config.duration);
|
||||
builder.play(updateAnim);
|
||||
mRecentsView.updateEmptyMessage();
|
||||
builder.addOnFinishRunnable(() -> mRecentsView.setHintVisibility(1));
|
||||
}
|
||||
|
||||
setAlphas(config.getPropertySetter(builder), toState.getVisibleElements(mLauncher));
|
||||
}
|
||||
|
||||
private void setAlphas(PropertySetter propertySetter, int visibleElements) {
|
||||
boolean hasClearAllButton = (visibleElements & RECENTS_CLEAR_ALL_BUTTON) != 0;
|
||||
propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
|
||||
hasClearAllButton ? 1f : 0f, LINEAR);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -18,11 +18,10 @@ package com.android.launcher3.uioverrides.states;
|
||||
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
|
||||
|
||||
import android.os.RemoteException;
|
||||
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.allapps.AllAppsTransitionController;
|
||||
import com.android.quickstep.RecentsModel;
|
||||
import com.android.quickstep.SysUINavigationMode;
|
||||
import com.android.quickstep.SysUINavigationMode.Mode;
|
||||
import com.android.quickstep.util.LayoutUtils;
|
||||
import com.android.quickstep.views.RecentsView;
|
||||
import com.android.systemui.shared.recents.ISystemUiProxy;
|
||||
@@ -75,4 +74,8 @@ public class BackgroundAppState extends OverviewState {
|
||||
return new ScaleAndTranslation(scale, 0f, 0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVisibleElements(Launcher launcher) {
|
||||
return super.getVisibleElements(launcher) & ~RECENTS_CLEAR_ALL_BUTTON;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -17,8 +17,6 @@ package com.android.launcher3.uioverrides.states;
|
||||
|
||||
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
|
||||
import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
|
||||
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
|
||||
import static com.android.launcher3.logging.LoggerUtils.getTargetStr;
|
||||
import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
|
||||
import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
|
||||
|
||||
@@ -27,7 +25,6 @@ import android.view.View;
|
||||
|
||||
import com.android.launcher3.AbstractFloatingView;
|
||||
import com.android.launcher3.DeviceProfile;
|
||||
import com.android.launcher3.InvariantDeviceProfile;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.LauncherState;
|
||||
import com.android.launcher3.R;
|
||||
@@ -73,6 +70,15 @@ public class OverviewState extends LauncherState {
|
||||
return new ScaleAndTranslation(scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
|
||||
if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
|
||||
// If the hotseat icons are visible in overview, keep them in their normal position.
|
||||
return super.getWorkspaceScaleAndTranslation(launcher);
|
||||
}
|
||||
return getWorkspaceScaleAndTranslation(launcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
|
||||
return new ScaleAndTranslation(1f, 0f, 0f);
|
||||
@@ -110,9 +116,9 @@ public class OverviewState extends LauncherState {
|
||||
@Override
|
||||
public int getVisibleElements(Launcher launcher) {
|
||||
if (launcher.getDeviceProfile().isVerticalBarLayout()) {
|
||||
return VERTICAL_SWIPE_INDICATOR;
|
||||
return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON;
|
||||
} else {
|
||||
return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR |
|
||||
return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON |
|
||||
(launcher.getAppsView().getFloatingHeaderView().hasVisibleContent()
|
||||
? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
|
||||
}
|
||||
|
||||
+6
-1
@@ -28,6 +28,7 @@ import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.LauncherState;
|
||||
@@ -44,12 +45,14 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
|
||||
private static final long PEEK_ANIM_DURATION = 100;
|
||||
|
||||
private final MotionPauseDetector mMotionPauseDetector;
|
||||
private final float mMotionPauseMinDisplacement;
|
||||
|
||||
private AnimatorSet mPeekAnim;
|
||||
|
||||
public FlingAndHoldTouchController(Launcher l) {
|
||||
super(l, false /* allowDragToOverview */);
|
||||
mMotionPauseDetector = new MotionPauseDetector(l);
|
||||
mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,7 +101,8 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
|
||||
|
||||
@Override
|
||||
public boolean onDrag(float displacement, MotionEvent event) {
|
||||
mMotionPauseDetector.addPosition(displacement, 0, event.getEventTime());
|
||||
mMotionPauseDetector.setDisallowPause(-displacement < mMotionPauseMinDisplacement);
|
||||
mMotionPauseDetector.addPosition(displacement, event.getEventTime());
|
||||
return super.onDrag(displacement, event);
|
||||
}
|
||||
|
||||
@@ -111,6 +115,7 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController {
|
||||
|
||||
AnimatorSetBuilder builder = new AnimatorSetBuilder();
|
||||
builder.setInterpolator(AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
|
||||
builder.setInterpolator(AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE, OVERSHOOT_1_2);
|
||||
AnimatorSet overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
|
||||
NORMAL, OVERVIEW, builder, ANIM_ALL, ATOMIC_DURATION);
|
||||
overviewAnim.addListener(new AnimatorListenerAdapter() {
|
||||
|
||||
+3
-3
@@ -17,8 +17,6 @@ package com.android.launcher3.uioverrides.touchcontrollers;
|
||||
|
||||
import static android.view.View.TRANSLATION_X;
|
||||
|
||||
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
|
||||
import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
|
||||
import static com.android.launcher3.LauncherState.ALL_APPS;
|
||||
import static com.android.launcher3.LauncherState.NORMAL;
|
||||
import static com.android.launcher3.LauncherState.OVERVIEW;
|
||||
@@ -44,9 +42,9 @@ import com.android.launcher3.anim.AnimationSuccessListener;
|
||||
import com.android.launcher3.anim.AnimatorPlaybackController;
|
||||
import com.android.launcher3.anim.AnimatorSetBuilder;
|
||||
import com.android.launcher3.anim.Interpolators;
|
||||
import com.android.launcher3.compat.AccessibilityManagerCompat;
|
||||
import com.android.launcher3.touch.SwipeDetector;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Command;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
|
||||
import com.android.launcher3.util.TouchController;
|
||||
import com.android.quickstep.views.RecentsView;
|
||||
@@ -212,11 +210,13 @@ public class NavBarToHomeTouchController implements TouchController, SwipeDetect
|
||||
private void onSwipeInteractionCompleted(LauncherState targetState) {
|
||||
clearState();
|
||||
mLauncher.getStateManager().goToState(targetState, false /* animated */);
|
||||
AccessibilityManagerCompat.sendStateEventToTest(mLauncher, targetState.ordinal);
|
||||
}
|
||||
|
||||
private void logStateChange(int startContainerType, int logAction) {
|
||||
mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
|
||||
LauncherLogProto.Action.Direction.UP,
|
||||
mSwipeDetector.getDownX(), mSwipeDetector.getDownY(),
|
||||
LauncherLogProto.ContainerType.NAVBAR,
|
||||
startContainerType,
|
||||
mEndState.containerType,
|
||||
|
||||
+2
-1
@@ -75,7 +75,8 @@ public final class PortraitOverviewStateTouchHelper {
|
||||
* @return the animation
|
||||
*/
|
||||
PendingAnimation createSwipeDownToTaskAppAnimation(long duration) {
|
||||
TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getNextPage());
|
||||
mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
|
||||
TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getCurrentPage());
|
||||
if (taskView == null) {
|
||||
throw new IllegalStateException("There is no task view to animate to.");
|
||||
}
|
||||
|
||||
+45
-21
@@ -21,10 +21,11 @@ import static android.view.MotionEvent.ACTION_DOWN;
|
||||
import static android.view.MotionEvent.ACTION_MOVE;
|
||||
import static android.view.MotionEvent.ACTION_POINTER_UP;
|
||||
import static android.view.MotionEvent.ACTION_UP;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE_NOOP;
|
||||
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction.UPLEFT;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction.UPRIGHT;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE_NOOP;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.NAVBAR;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
@@ -35,12 +36,15 @@ import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.android.launcher3.BaseDraggingActivity;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.anim.Interpolators;
|
||||
import com.android.launcher3.logging.UserEventDispatcher;
|
||||
import com.android.quickstep.util.MotionPauseDetector;
|
||||
import com.android.systemui.shared.recents.ISystemUiProxy;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.systemui.shared.system.InputMonitorCompat;
|
||||
import com.android.systemui.shared.system.NavigationBarCompat;
|
||||
|
||||
/**
|
||||
@@ -76,27 +80,32 @@ public class AssistantTouchConsumer implements InputConsumer {
|
||||
private float mLastProgress;
|
||||
private int mState;
|
||||
private int mDirection;
|
||||
private ActivityControlHelper mActivityControlHelper;
|
||||
|
||||
private final float mDistThreshold;
|
||||
private final long mTimeThreshold;
|
||||
private final int mAngleThreshold;
|
||||
private final float mSlop;
|
||||
private final MotionPauseDetector mMotionPauseDetector;
|
||||
private final ISystemUiProxy mSysUiProxy;
|
||||
private final InputConsumer mConsumerDelegate;
|
||||
private final Context mContext;
|
||||
|
||||
private final InputMonitorCompat mInputMonitorCompat;
|
||||
|
||||
|
||||
public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy,
|
||||
InputConsumer delegate) {
|
||||
InputConsumer delegate, InputMonitorCompat inputMonitorCompat,
|
||||
ActivityControlHelper activityControlHelper) {
|
||||
final Resources res = context.getResources();
|
||||
mContext = context;
|
||||
mSysUiProxy = systemUiProxy;
|
||||
mConsumerDelegate = delegate;
|
||||
mMotionPauseDetector = new MotionPauseDetector(context);
|
||||
mDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
|
||||
mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold);
|
||||
mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
|
||||
mSlop = NavigationBarCompat.getQuickScrubTouchSlopPx();
|
||||
mSlop = NavigationBarCompat.getQuickStepDragSlopPx();
|
||||
mInputMonitorCompat = inputMonitorCompat;
|
||||
mActivityControlHelper = activityControlHelper;
|
||||
mState = STATE_INACTIVE;
|
||||
}
|
||||
|
||||
@@ -106,8 +115,18 @@ public class AssistantTouchConsumer implements InputConsumer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return mState != STATE_INACTIVE;
|
||||
public boolean useSharedSwipeState() {
|
||||
if (mConsumerDelegate != null) {
|
||||
return mConsumerDelegate.useSharedSwipeState();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConsumerAboutToBeSwitched() {
|
||||
if (mConsumerDelegate != null) {
|
||||
mConsumerDelegate.onConsumerAboutToBeSwitched();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -120,14 +139,6 @@ public class AssistantTouchConsumer implements InputConsumer {
|
||||
mDownPos.set(ev.getX(), ev.getY());
|
||||
mLastPos.set(mDownPos);
|
||||
mTimeFraction = 0;
|
||||
|
||||
// Detect when the gesture decelerates to start the assistant
|
||||
mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
|
||||
if (isPaused && mState == STATE_ASSISTANT_ACTIVE) {
|
||||
mTimeFraction = 1;
|
||||
updateAssistantProgress();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ACTION_POINTER_UP: {
|
||||
@@ -156,6 +167,10 @@ public class AssistantTouchConsumer implements InputConsumer {
|
||||
if (!mPassedSlop) {
|
||||
// Normal gesture, ensure we pass the slop before we start tracking the gesture
|
||||
if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) > mSlop) {
|
||||
|
||||
// Cancel touches to other windows (intercept)
|
||||
mInputMonitorCompat.pilferPointers();
|
||||
|
||||
mPassedSlop = true;
|
||||
mStartDragPos.set(mLastPos.x, mLastPos.y);
|
||||
mDragTime = SystemClock.uptimeMillis();
|
||||
@@ -165,7 +180,8 @@ public class AssistantTouchConsumer implements InputConsumer {
|
||||
Math.atan2(mDownPos.y - mLastPos.y, mDownPos.x - mLastPos.x));
|
||||
mDirection = angle > 90 ? UPLEFT : UPRIGHT;
|
||||
angle = angle > 90 ? 180 - angle : angle;
|
||||
if (angle > mAngleThreshold) {
|
||||
|
||||
if (angle > mAngleThreshold && angle < 90) {
|
||||
mState = STATE_ASSISTANT_ACTIVE;
|
||||
|
||||
if (mConsumerDelegate != null) {
|
||||
@@ -183,7 +199,6 @@ public class AssistantTouchConsumer implements InputConsumer {
|
||||
// Movement
|
||||
mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
|
||||
mLastPos.y - mStartDragPos.y);
|
||||
mMotionPauseDetector.addPosition(mDistance, 0, ev.getEventTime());
|
||||
if (mDistance >= 0) {
|
||||
final long diff = SystemClock.uptimeMillis() - mDragTime;
|
||||
mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
|
||||
@@ -212,7 +227,8 @@ public class AssistantTouchConsumer implements InputConsumer {
|
||||
animator.setInterpolator(Interpolators.DEACCEL_2);
|
||||
animator.start();
|
||||
}
|
||||
mMotionPauseDetector.clear();
|
||||
mPassedSlop = false;
|
||||
mState = STATE_INACTIVE;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -232,6 +248,14 @@ public class AssistantTouchConsumer implements InputConsumer {
|
||||
SWIPE, mDirection, NAVBAR);
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
|
||||
|
||||
BaseDraggingActivity launcherActivity = mActivityControlHelper.getCreatedActivity();
|
||||
if (launcherActivity != null) {
|
||||
launcherActivity.getRootView().
|
||||
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
|
||||
}
|
||||
|
||||
mSysUiProxy.startAssistant(args);
|
||||
mLaunchedAssistant = true;
|
||||
}
|
||||
|
||||
+18
-4
@@ -17,7 +17,9 @@ package com.android.quickstep;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PointF;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
/**
|
||||
* A dummy input consumer used when the device is still locked, e.g. from secure camera.
|
||||
@@ -25,9 +27,13 @@ import android.view.MotionEvent;
|
||||
public class DeviceLockedInputConsumer implements InputConsumer {
|
||||
|
||||
private final Context mContext;
|
||||
private final float mTouchSlopSquared;
|
||||
private final PointF mTouchDown = new PointF();
|
||||
|
||||
public DeviceLockedInputConsumer(Context context) {
|
||||
mContext = context;
|
||||
float touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||
mTouchSlopSquared = touchSlop * touchSlop;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,11 +43,19 @@ public class DeviceLockedInputConsumer implements InputConsumer {
|
||||
|
||||
@Override
|
||||
public void onMotionEvent(MotionEvent ev) {
|
||||
// For now, just start the home intent so user is prompted to unlock the device.
|
||||
float x = ev.getX();
|
||||
float y = ev.getY();
|
||||
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
mContext.startActivity(new Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_HOME)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
mTouchDown.set(x, y);
|
||||
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
float xSquared = (x - mTouchDown.x) * (x - mTouchDown.x);
|
||||
float ySquared = (y - mTouchDown.y) * (y - mTouchDown.y);
|
||||
if (xSquared + ySquared > mTouchSlopSquared) {
|
||||
// For now, just start the home intent so user is prompted to unlock the device.
|
||||
mContext.startActivity(new Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_HOME)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -26,11 +26,13 @@ import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.launcher3.DeviceProfile;
|
||||
import com.android.launcher3.anim.AnimationSuccessListener;
|
||||
import com.android.launcher3.anim.AnimatorPlaybackController;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto;
|
||||
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
|
||||
import com.android.quickstep.util.LayoutUtils;
|
||||
import com.android.quickstep.util.RemoteAnimationTargetSet;
|
||||
import com.android.quickstep.views.RecentsView;
|
||||
@@ -39,9 +41,6 @@ import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* {@link ActivityControlHelper} for recents when the default launcher is different than the
|
||||
* currently running one and apps should interact with the {@link RecentsActivity} as opposed
|
||||
@@ -72,7 +71,9 @@ public final class FallbackActivityControllerHelper implements
|
||||
|
||||
@Override
|
||||
public void onSwipeUpComplete(RecentsActivity activity) {
|
||||
// TODO:
|
||||
RecentsView recentsView = activity.getOverviewPanel();
|
||||
recentsView.getClearAllButton().setVisibilityAlpha(1);
|
||||
recentsView.setDisallowScrollToClearAll(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,6 +122,8 @@ public final class FallbackActivityControllerHelper implements
|
||||
|
||||
RecentsView rv = activity.getOverviewPanel();
|
||||
rv.setContentAlpha(0);
|
||||
rv.getClearAllButton().setVisibilityAlpha(0);
|
||||
rv.setDisallowScrollToClearAll(true);
|
||||
|
||||
return new AnimationFactory() {
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public interface InputConsumer {
|
||||
|
||||
int getType();
|
||||
|
||||
default boolean isActive() {
|
||||
default boolean useSharedSwipeState() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+17
-32
@@ -38,10 +38,14 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.android.launcher3.DeviceProfile;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.LauncherAppState;
|
||||
import com.android.launcher3.LauncherInitListener;
|
||||
import com.android.launcher3.LauncherInitListenerEx;
|
||||
import com.android.launcher3.LauncherState;
|
||||
import com.android.launcher3.allapps.DiscoveryBounce;
|
||||
import com.android.launcher3.anim.AnimatorPlaybackController;
|
||||
@@ -59,10 +63,6 @@ import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
/**
|
||||
* {@link ActivityControlHelper} for the in-launcher recents.
|
||||
*/
|
||||
@@ -163,21 +163,16 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
|
||||
}
|
||||
activity.getStateManager().setRestState(resetState);
|
||||
|
||||
final LauncherState fromState;
|
||||
if (!activityVisible) {
|
||||
// Since the launcher is not visible, we can safely reset the scroll position.
|
||||
// This ensures then the next swipe up to all-apps starts from scroll 0.
|
||||
activity.getAppsView().reset(false /* animate */);
|
||||
fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
|
||||
activity.getStateManager().goToState(fromState, false);
|
||||
final LauncherState fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
|
||||
activity.getStateManager().goToState(fromState, false);
|
||||
// Since all apps is not visible, we can safely reset the scroll position.
|
||||
// This ensures then the next swipe up to all-apps starts from scroll 0.
|
||||
activity.getAppsView().reset(false /* animate */);
|
||||
|
||||
// Optimization, hide the all apps view to prevent layout while initializing
|
||||
activity.getAppsView().getContentView().setVisibility(View.GONE);
|
||||
// Optimization, hide the all apps view to prevent layout while initializing
|
||||
activity.getAppsView().getContentView().setVisibility(View.GONE);
|
||||
|
||||
AccessibilityManagerCompat.sendStateEventToTest(activity, fromState.ordinal);
|
||||
} else {
|
||||
fromState = startState;
|
||||
}
|
||||
AccessibilityManagerCompat.sendStateEventToTest(activity, fromState.ordinal);
|
||||
|
||||
return new AnimationFactory() {
|
||||
private Animator mShelfAnim;
|
||||
@@ -185,8 +180,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
|
||||
|
||||
@Override
|
||||
public void createActivityController(long transitionLength) {
|
||||
createActivityControllerInternal(activity, activityVisible, fromState,
|
||||
transitionLength, callback);
|
||||
createActivityControllerInternal(activity, fromState, transitionLength, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -230,18 +224,9 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
|
||||
};
|
||||
}
|
||||
|
||||
private void createActivityControllerInternal(Launcher activity, boolean wasVisible,
|
||||
LauncherState fromState, long transitionLength,
|
||||
Consumer<AnimatorPlaybackController> callback) {
|
||||
private void createActivityControllerInternal(Launcher activity, LauncherState fromState,
|
||||
long transitionLength, Consumer<AnimatorPlaybackController> callback) {
|
||||
LauncherState endState = OVERVIEW;
|
||||
if (wasVisible && fromState != BACKGROUND_APP) {
|
||||
// If a translucent app was launched fom launcher, animate launcher states.
|
||||
DeviceProfile dp = activity.getDeviceProfile();
|
||||
long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
|
||||
callback.accept(activity.getStateManager()
|
||||
.createAnimationToNewWorkspace(fromState, endState, accuracy));
|
||||
return;
|
||||
}
|
||||
if (fromState == endState) {
|
||||
return;
|
||||
}
|
||||
@@ -319,7 +304,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
|
||||
@Override
|
||||
public ActivityInitListener createActivityInitListener(
|
||||
BiPredicate<Launcher, Boolean> onInitListener) {
|
||||
return new LauncherInitListener(onInitListener);
|
||||
return new LauncherInitListenerEx(onInitListener);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
+23
-14
@@ -21,9 +21,9 @@ import static android.view.MotionEvent.ACTION_MOVE;
|
||||
import static android.view.MotionEvent.ACTION_POINTER_UP;
|
||||
import static android.view.MotionEvent.ACTION_UP;
|
||||
import static android.view.MotionEvent.INVALID_POINTER_ID;
|
||||
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
|
||||
import static com.android.launcher3.util.RaceConditionTracker.ENTER;
|
||||
import static com.android.launcher3.util.RaceConditionTracker.EXIT;
|
||||
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
|
||||
import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
|
||||
import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
|
||||
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
|
||||
@@ -45,6 +45,9 @@ import android.view.VelocityTracker;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.util.Preconditions;
|
||||
import com.android.launcher3.util.RaceConditionTracker;
|
||||
import com.android.launcher3.util.TraceHelper;
|
||||
@@ -62,8 +65,6 @@ import com.android.systemui.shared.system.WindowManagerWrapper;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
/**
|
||||
* Input consumer for handling events originating from an activity other than Launcher
|
||||
*/
|
||||
@@ -90,6 +91,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
|
||||
private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
|
||||
private final MotionPauseDetector mMotionPauseDetector;
|
||||
private final float mMotionPauseMinDisplacement;
|
||||
private VelocityTracker mVelocityTracker;
|
||||
|
||||
private WindowTransformSwipeHandler mInteractionHandler;
|
||||
@@ -107,8 +109,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
// Slop used to determine when we say that the gesture has started.
|
||||
private boolean mPassedTouchSlop;
|
||||
|
||||
// TODO: Start displacement should have both x and y
|
||||
// Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
|
||||
private float mStartDisplacement;
|
||||
private float mStartDisplacementX;
|
||||
|
||||
private Handler mMainThreadHandler;
|
||||
private Runnable mCancelRecentsAnimationRunnable = () -> {
|
||||
@@ -131,6 +134,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
mMode = SysUINavigationMode.getMode(base);
|
||||
|
||||
mMotionPauseDetector = new MotionPauseDetector(base);
|
||||
mMotionPauseMinDisplacement = base.getResources().getDimension(
|
||||
R.dimen.motion_pause_detector_min_displacement_from_app);
|
||||
mOnCompleteCallback = onCompleteCallback;
|
||||
mVelocityTracker = VelocityTracker.obtain();
|
||||
mInputMonitorCompat = inputMonitorCompat;
|
||||
@@ -149,6 +154,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
|
||||
mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx();
|
||||
mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx();
|
||||
|
||||
mPassedTouchSlop = mPassedDragSlop = continuingPreviousGesture;
|
||||
}
|
||||
|
||||
@@ -218,6 +224,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
}
|
||||
mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
|
||||
float displacement = getDisplacement(ev);
|
||||
float displacementX = mLastPos.x - mDownPos.x;
|
||||
|
||||
if (!mPassedDragSlop) {
|
||||
if (!mIsDeferredDownTarget) {
|
||||
@@ -226,13 +233,13 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
if (Math.abs(displacement) > mDragSlop) {
|
||||
mPassedDragSlop = true;
|
||||
mStartDisplacement = displacement;
|
||||
mStartDisplacementX = displacementX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mPassedTouchSlop) {
|
||||
if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) >=
|
||||
mTouchSlop) {
|
||||
if (Math.hypot(displacementX, mLastPos.y - mDownPos.y) >= mTouchSlop) {
|
||||
mPassedTouchSlop = true;
|
||||
|
||||
if (mIsDeferredDownTarget) {
|
||||
@@ -243,6 +250,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
if (!mPassedDragSlop) {
|
||||
mPassedDragSlop = true;
|
||||
mStartDisplacement = displacement;
|
||||
mStartDisplacementX = displacementX;
|
||||
}
|
||||
notifyGestureStarted();
|
||||
}
|
||||
@@ -253,12 +261,12 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
|
||||
|
||||
if (mMode == Mode.NO_BUTTON) {
|
||||
boolean isLandscape = isNavBarOnLeft() || isNavBarOnRight();
|
||||
float orthogonalDisplacement = !isLandscape
|
||||
? ev.getX() - mDownPos.x
|
||||
: ev.getY() - mDownPos.y;
|
||||
mMotionPauseDetector.addPosition(displacement, orthogonalDisplacement,
|
||||
ev.getEventTime());
|
||||
float horizontalDist = Math.abs(displacementX - mStartDisplacementX);
|
||||
float upDist = -(displacement - mStartDisplacement);
|
||||
boolean isLikelyToStartNewTask = horizontalDist > upDist;
|
||||
mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
|
||||
|| isLikelyToStartNewTask);
|
||||
mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -346,7 +354,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
: velocityY;
|
||||
|
||||
mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
|
||||
mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY));
|
||||
mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY),
|
||||
mDownPos);
|
||||
} else {
|
||||
// Since we start touch tracking on DOWN, we may reach this state without actually
|
||||
// starting the gesture. In that case, just cleanup immediately.
|
||||
@@ -412,7 +421,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
public boolean useSharedSwipeState() {
|
||||
return mInteractionHandler != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ import java.util.List;
|
||||
*/
|
||||
public class TaskOverlayFactory implements ResourceBasedOverride {
|
||||
|
||||
public static final String AIAI_PACKAGE = "com.google.android.as";
|
||||
|
||||
/** Note that these will be shown in order from top to bottom, if available for the task. */
|
||||
private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{
|
||||
new TaskSystemShortcut.AppInfo(),
|
||||
|
||||
@@ -38,7 +38,7 @@ import com.android.launcher3.ItemInfo;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.LauncherState;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.ShortcutInfo;
|
||||
import com.android.launcher3.WorkspaceItemInfo;
|
||||
import com.android.launcher3.popup.SystemShortcut;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto;
|
||||
import com.android.launcher3.util.InstantAppResolver;
|
||||
@@ -86,7 +86,7 @@ public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut
|
||||
public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) {
|
||||
Task task = view.getTask();
|
||||
|
||||
ShortcutInfo dummyInfo = new ShortcutInfo();
|
||||
WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
|
||||
dummyInfo.intent = new Intent();
|
||||
ComponentName component = task.getTopComponent();
|
||||
dummyInfo.intent.setComponent(component);
|
||||
|
||||
+9
-4
@@ -87,7 +87,7 @@ public class TouchInteractionService extends Service implements
|
||||
new LooperExecutor(UiThreadHelper.getBackgroundLooper());
|
||||
|
||||
private static final String NAVBAR_VERTICAL_SIZE = "navigation_bar_frame_height";
|
||||
private static final String NAVBAR_HORIZONTAL_SIZE = "navigation_bar_frame_width";
|
||||
private static final String NAVBAR_HORIZONTAL_SIZE = "navigation_bar_width";
|
||||
|
||||
public static final EventLogArray TOUCH_INTERACTION_LOG =
|
||||
new EventLogArray("touch_interaction_log", 40);
|
||||
@@ -422,7 +422,7 @@ public class TouchInteractionService extends Service implements
|
||||
if (event.getAction() == ACTION_DOWN) {
|
||||
if (isInValidSystemUiState()
|
||||
&& mSwipeTouchRegion.contains(event.getX(), event.getY())) {
|
||||
boolean useSharedState = mConsumer.isActive();
|
||||
boolean useSharedState = mConsumer.useSharedSwipeState();
|
||||
mConsumer.onConsumerAboutToBeSwitched();
|
||||
mConsumer = newConsumer(useSharedState, event);
|
||||
TOUCH_INTERACTION_LOG.addLog("setInputConsumer", mConsumer.getType());
|
||||
@@ -455,13 +455,18 @@ public class TouchInteractionService extends Service implements
|
||||
|
||||
final ActivityControlHelper activityControl =
|
||||
mOverviewComponentObserver.getActivityControlHelper();
|
||||
|
||||
if (runningTaskInfo == null && !mSwipeSharedState.goingToLauncher) {
|
||||
return InputConsumer.NO_OP;
|
||||
} else if (mAssistantAvailable
|
||||
&& SysUINavigationMode.INSTANCE.get(this).getMode() == Mode.NO_BUTTON
|
||||
&& AssistantTouchConsumer.withinTouchRegion(this, event)) {
|
||||
return new AssistantTouchConsumer(this, mISystemUiProxy, !activityControl.isResumed()
|
||||
? createOtherActivityInputConsumer(event, runningTaskInfo) : null);
|
||||
|
||||
boolean addDelegate = !activityControl.isResumed();
|
||||
return new AssistantTouchConsumer(this, mISystemUiProxy, addDelegate ?
|
||||
createOtherActivityInputConsumer(event, runningTaskInfo) : null,
|
||||
mInputMonitorCompat, activityControl);
|
||||
|
||||
} else if (mSwipeSharedState.goingToLauncher || activityControl.isResumed()) {
|
||||
return OverviewInputConsumer.newInstance(activityControl, false);
|
||||
} else if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
|
||||
|
||||
+28
-17
@@ -64,6 +64,9 @@ import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.android.launcher3.AbstractFloatingView;
|
||||
import com.android.launcher3.BaseDraggingActivity;
|
||||
import com.android.launcher3.DeviceProfile;
|
||||
@@ -103,9 +106,6 @@ import com.android.systemui.shared.system.WindowCallbacksCompat;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
|
||||
implements SwipeAnimationListener, OnApplyWindowInsetsListener {
|
||||
@@ -253,6 +253,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
|
||||
private boolean mGestureStarted;
|
||||
private int mLogAction = Touch.SWIPE;
|
||||
private int mLogDirection = Direction.UP;
|
||||
private PointF mDownPos;
|
||||
|
||||
private final RecentsAnimationWrapper mRecentsAnimationWrapper;
|
||||
|
||||
@@ -429,8 +430,19 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
|
||||
// If we've already ended the gesture and are going home, don't prepare recents UI,
|
||||
// as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
|
||||
if (mGestureEndTarget != HOME) {
|
||||
mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
|
||||
mWasLauncherAlreadyVisible, true, this::onAnimatorPlaybackControllerCreated);
|
||||
Runnable initAnimFactory = () -> {
|
||||
mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
|
||||
mWasLauncherAlreadyVisible, true,
|
||||
this::onAnimatorPlaybackControllerCreated);
|
||||
};
|
||||
if (mWasLauncherAlreadyVisible) {
|
||||
// Launcher is visible, but might be about to stop. Thus, if we prepare recents
|
||||
// now, it might get overridden by moveToRestState() in onStop(). To avoid this,
|
||||
// wait until the next gesture (and possibly launcher) starts.
|
||||
mStateCallback.addCallback(STATE_GESTURE_STARTED, initAnimFactory);
|
||||
} else {
|
||||
initAnimFactory.run();
|
||||
}
|
||||
}
|
||||
AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible);
|
||||
|
||||
@@ -692,9 +704,10 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
|
||||
/**
|
||||
* @param endVelocity The velocity in the direction of the nav bar to the middle of the screen.
|
||||
* @param velocity The x and y components of the velocity when the gesture ends.
|
||||
* @param downPos The x and y value of where the gesture started.
|
||||
*/
|
||||
@UiThread
|
||||
public void onGestureEnded(float endVelocity, PointF velocity) {
|
||||
public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
|
||||
float flingThreshold = mContext.getResources()
|
||||
.getDimension(R.dimen.quickstep_fling_threshold_velocity);
|
||||
boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
|
||||
@@ -707,6 +720,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
|
||||
} else {
|
||||
mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT;
|
||||
}
|
||||
mDownPos = downPos;
|
||||
handleNormalGestureEnd(endVelocity, isFling, velocity);
|
||||
}
|
||||
|
||||
@@ -733,14 +747,10 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
|
||||
float endShift;
|
||||
final float startShift;
|
||||
Interpolator interpolator = DEACCEL;
|
||||
int nextPage = 0;
|
||||
int taskToLaunch = 0;
|
||||
final boolean goingToNewTask;
|
||||
if (mRecentsView != null) {
|
||||
nextPage = mRecentsView.getNextPage();
|
||||
final int lastTaskIndex = mRecentsView.getTaskViewCount() - 1;
|
||||
final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
|
||||
taskToLaunch = nextPage <= lastTaskIndex ? nextPage : lastTaskIndex;
|
||||
final int taskToLaunch = mRecentsView.getNextPage();
|
||||
goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
|
||||
} else {
|
||||
goingToNewTask = false;
|
||||
@@ -806,11 +816,6 @@ public class WindowTransformSwipeHandler<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) {
|
||||
setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
|
||||
duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
|
||||
@@ -845,6 +850,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
|
||||
: mRecentsView.getNextPage();
|
||||
UserEventDispatcher.newInstance(mContext).logStateChangeAction(
|
||||
mLogAction, mLogDirection,
|
||||
(int) mDownPos.x, (int) mDownPos.y,
|
||||
ContainerType.NAVBAR, ContainerType.APP,
|
||||
endTarget.containerType,
|
||||
pageIndex);
|
||||
@@ -1044,7 +1050,12 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
|
||||
}
|
||||
|
||||
private void invalidateHandlerWithLauncher() {
|
||||
mLauncherTransitionController = null;
|
||||
if (mLauncherTransitionController != null) {
|
||||
if (mLauncherTransitionController.getAnimationPlayer().isStarted()) {
|
||||
mLauncherTransitionController.getAnimationPlayer().cancel();
|
||||
}
|
||||
mLauncherTransitionController = null;
|
||||
}
|
||||
|
||||
mRecentsView.setEnableFreeScroll(true);
|
||||
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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -102,7 +102,7 @@ public class ClipAnimationHelper {
|
||||
public ClipAnimationHelper(Context context) {
|
||||
mWindowCornerRadius = getWindowCornerRadius(context.getResources());
|
||||
mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources());
|
||||
mTaskCornerRadius = Themes.getDialogCornerRadius(context);
|
||||
mTaskCornerRadius = TaskCornerRadius.get(context);
|
||||
}
|
||||
|
||||
private void updateSourceStack(RemoteAnimationTargetCompat target) {
|
||||
|
||||
@@ -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.util.AttributeSet;
|
||||
import android.util.Property;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.android.launcher3.Utilities;
|
||||
@@ -26,8 +27,22 @@ import com.android.quickstep.views.RecentsView.ScrollState;
|
||||
|
||||
public class ClearAllButton extends Button implements PageCallbacks {
|
||||
|
||||
public static final Property<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 mContentAlpha = 1;
|
||||
private float mVisibilityAlpha = 1;
|
||||
|
||||
private final boolean mIsRtl;
|
||||
|
||||
@@ -58,6 +73,13 @@ public class ClearAllButton extends Button implements PageCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisibilityAlpha(float alpha) {
|
||||
if (mVisibilityAlpha != alpha) {
|
||||
mVisibilityAlpha = alpha;
|
||||
updateAlpha();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScroll(ScrollState scrollState) {
|
||||
float width = getWidth();
|
||||
@@ -72,7 +94,7 @@ public class ClearAllButton extends Button implements PageCallbacks {
|
||||
}
|
||||
|
||||
private void updateAlpha() {
|
||||
final float alpha = mScrollAlpha * mContentAlpha;
|
||||
final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha;
|
||||
setAlpha(alpha);
|
||||
setClickable(alpha == 1);
|
||||
}
|
||||
|
||||
+4
-5
@@ -32,7 +32,6 @@ import android.os.UserHandle;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -59,7 +58,6 @@ public final class DigitalWellBeingToast extends LinearLayout {
|
||||
private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
|
||||
|
||||
private Task mTask;
|
||||
private ImageView mImage;
|
||||
private TextView mText;
|
||||
|
||||
public DigitalWellBeingToast(Context context, AttributeSet attrs) {
|
||||
@@ -70,12 +68,15 @@ public final class DigitalWellBeingToast extends LinearLayout {
|
||||
mLauncherApps = context.getSystemService(LauncherApps.class);
|
||||
}
|
||||
|
||||
public TextView getTextView() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
mText = findViewById(R.id.digital_well_being_remaining_time);
|
||||
mImage = findViewById(R.id.digital_well_being_hourglass);
|
||||
}
|
||||
|
||||
public void initialize(Task task, InitializeCallback callback) {
|
||||
@@ -103,8 +104,6 @@ public final class DigitalWellBeingToast extends LinearLayout {
|
||||
} else {
|
||||
setVisibility(VISIBLE);
|
||||
mText.setText(getText(appRemainingTimeMs));
|
||||
mImage.setImageResource(appRemainingTimeMs > 0 ?
|
||||
R.drawable.hourglass_top : R.drawable.hourglass_bottom);
|
||||
}
|
||||
|
||||
callback.call(getContentDescriptionForTask(
|
||||
|
||||
+23
-54
@@ -19,9 +19,9 @@ import static com.android.launcher3.LauncherState.ALL_APPS;
|
||||
import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
|
||||
import static com.android.launcher3.LauncherState.NORMAL;
|
||||
import static com.android.launcher3.LauncherState.OVERVIEW;
|
||||
import static com.android.launcher3.LauncherState.RECENTS_CLEAR_ALL_BUTTON;
|
||||
import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
|
||||
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
|
||||
import static com.android.launcher3.config.FeatureFlags.ENABLE_HINTS_IN_OVERVIEW;
|
||||
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
|
||||
|
||||
import android.animation.AnimatorSet;
|
||||
@@ -39,11 +39,10 @@ import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.LauncherState;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.anim.Interpolators;
|
||||
import com.android.launcher3.util.PendingAnimation;
|
||||
import com.android.launcher3.views.BaseDragLayer;
|
||||
import com.android.launcher3.appprediction.PredictionUiStateManager;
|
||||
import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
|
||||
import com.android.launcher3.views.ScrimView;
|
||||
import com.android.quickstep.SysUINavigationMode;
|
||||
import com.android.quickstep.hints.ChipsContainer;
|
||||
import com.android.quickstep.util.ClipAnimationHelper;
|
||||
import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
|
||||
import com.android.quickstep.util.LayoutUtils;
|
||||
@@ -55,7 +54,6 @@ import com.android.quickstep.util.LayoutUtils;
|
||||
public class LauncherRecentsView extends RecentsView<Launcher> {
|
||||
|
||||
private final TransformParams mTransformParams = new TransformParams();
|
||||
private ChipsContainer mChipsContainer;
|
||||
|
||||
public LauncherRecentsView(Context context) {
|
||||
this(context, null);
|
||||
@@ -75,14 +73,6 @@ public class LauncherRecentsView extends RecentsView<Launcher> {
|
||||
mActivity.getStateManager().goToState(NORMAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
mChipsContainer = mActivity.findViewById(R.id.hints);
|
||||
BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) mChipsContainer.getLayoutParams();
|
||||
params.bottomMargin = mActivity.getDeviceProfile().chipHintBottomMarginPx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTranslationY(float translationY) {
|
||||
super.setTranslationY(translationY);
|
||||
@@ -94,16 +84,6 @@ public class LauncherRecentsView extends RecentsView<Launcher> {
|
||||
}
|
||||
}
|
||||
|
||||
public void setHintVisibility(float v) {
|
||||
if (mChipsContainer != null && ENABLE_HINTS_IN_OVERVIEW.get()) {
|
||||
mChipsContainer.setHintVisibility(v);
|
||||
}
|
||||
}
|
||||
|
||||
public ChipsContainer getChipsContainer() {
|
||||
return mChipsContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
maybeDrawEmptyMessage(canvas);
|
||||
@@ -154,37 +134,6 @@ public class LauncherRecentsView extends RecentsView<Launcher> {
|
||||
return anim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) {
|
||||
PendingAnimation anim = super.createTaskLauncherAnimation(tv, duration);
|
||||
|
||||
if (ENABLE_HINTS_IN_OVERVIEW.get()) {
|
||||
anim.anim.play(ObjectAnimator.ofFloat(
|
||||
mChipsContainer, ChipsContainer.HINT_VISIBILITY, 0));
|
||||
}
|
||||
|
||||
return anim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
|
||||
boolean shouldRemoveTask, long duration) {
|
||||
PendingAnimation anim = super.createTaskDismissAnimation(taskView, animateTaskView,
|
||||
shouldRemoveTask, duration);
|
||||
|
||||
if (ENABLE_HINTS_IN_OVERVIEW.get()) {
|
||||
anim.anim.play(ObjectAnimator.ofFloat(
|
||||
mChipsContainer, ChipsContainer.HINT_VISIBILITY, 0));
|
||||
anim.addEndListener(onEndListener -> {
|
||||
if (!onEndListener.isSuccess) {
|
||||
mChipsContainer.setHintVisibility(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return anim;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getTaskSize(DeviceProfile dp, Rect outRect) {
|
||||
LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
|
||||
@@ -247,4 +196,24 @@ public class LauncherRecentsView extends RecentsView<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.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
|
||||
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
|
||||
import static com.android.launcher3.anim.Interpolators.ACCEL;
|
||||
import static com.android.launcher3.anim.Interpolators.ACCEL_2;
|
||||
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
|
||||
@@ -25,9 +26,10 @@ import static com.android.launcher3.anim.Interpolators.LINEAR;
|
||||
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
|
||||
import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
|
||||
import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
|
||||
import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
|
||||
import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
|
||||
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
|
||||
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
|
||||
import static com.android.quickstep.util.ClipAnimationHelper.TransformParams;
|
||||
|
||||
import android.animation.Animator;
|
||||
@@ -46,6 +48,7 @@ import android.graphics.Canvas;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
@@ -89,7 +92,6 @@ import com.android.launcher3.util.OverScroller;
|
||||
import com.android.launcher3.util.PendingAnimation;
|
||||
import com.android.launcher3.util.Themes;
|
||||
import com.android.launcher3.util.ViewPool;
|
||||
import com.android.quickstep.OverviewCallbacks;
|
||||
import com.android.quickstep.RecentsAnimationWrapper;
|
||||
import com.android.quickstep.RecentsModel;
|
||||
import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener;
|
||||
@@ -169,6 +171,7 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
|
||||
private final ViewPool<TaskView> mTaskViewPool;
|
||||
|
||||
private boolean mDwbToastShown;
|
||||
private boolean mDisallowScrollToClearAll;
|
||||
|
||||
/**
|
||||
* 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.setTextSize(getResources()
|
||||
.getDimension(R.dimen.recents_empty_message_text_size));
|
||||
mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
|
||||
Typeface.NORMAL));
|
||||
mEmptyMessagePadding = getResources()
|
||||
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
|
||||
setWillNotDraw(false);
|
||||
@@ -748,8 +753,6 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
|
||||
|
||||
unloadVisibleTaskData();
|
||||
setCurrentPage(0);
|
||||
|
||||
OverviewCallbacks.get(getContext()).onResetOverview();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1159,6 +1162,7 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
|
||||
@SuppressWarnings("unused")
|
||||
private void dismissAllTasks(View view) {
|
||||
runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
|
||||
mActivity.getUserEventDispatcher().logActionOnControl(TAP, CLEAR_ALL_BUTTON);
|
||||
}
|
||||
|
||||
private void dismissCurrentTask() {
|
||||
@@ -1606,4 +1610,33 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
|
||||
|
||||
mRecentsAnimationWrapper.finish(toRecents, onFinishComplete);
|
||||
}
|
||||
|
||||
public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
|
||||
if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
|
||||
mDisallowScrollToClearAll = disallowScrollToClearAll;
|
||||
updateMinAndMaxScrollX();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeMinScrollX() {
|
||||
if (mIsRtl && mDisallowScrollToClearAll) {
|
||||
// We aren't showing the clear all button, so use the leftmost task as the min scroll.
|
||||
return getScrollForPage(getTaskViewCount() - 1);
|
||||
}
|
||||
return super.computeMinScrollX();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeMaxScrollX() {
|
||||
if (!mIsRtl && mDisallowScrollToClearAll) {
|
||||
// We aren't showing the clear all button, so use the rightmost task as the max scroll.
|
||||
return getScrollForPage(getTaskViewCount() - 1);
|
||||
}
|
||||
return super.computeMaxScrollX();
|
||||
}
|
||||
|
||||
public ClearAllButton getClearAllButton() {
|
||||
return mClearAllButton;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -48,6 +48,7 @@ import com.android.launcher3.util.SystemUiController;
|
||||
import com.android.launcher3.util.Themes;
|
||||
import com.android.quickstep.TaskOverlayFactory;
|
||||
import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
|
||||
import com.android.quickstep.util.TaskCornerRadius;
|
||||
import com.android.systemui.shared.recents.model.Task;
|
||||
import com.android.systemui.shared.recents.model.ThumbnailData;
|
||||
import com.android.systemui.shared.system.QuickStepContract;
|
||||
@@ -108,7 +109,7 @@ public class TaskThumbnailView extends View {
|
||||
|
||||
public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
mCornerRadius = Themes.getDialogCornerRadius(context);
|
||||
mCornerRadius = TaskCornerRadius.get(context);
|
||||
mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
|
||||
mPaint.setFilterBitmap(true);
|
||||
mBackgroundPaint.setColor(Color.WHITE);
|
||||
|
||||
@@ -211,6 +211,10 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
return mMenuView;
|
||||
}
|
||||
|
||||
public DigitalWellBeingToast getDigitalWellBeingToast() {
|
||||
return mDigitalWellBeingToast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this task view to the given {@param task}.
|
||||
*/
|
||||
|
||||
@@ -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
-1
@@ -2,7 +2,8 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
<group>
|
||||
<clip-path android:pathData="M0,0H24V24H0Z M 0,0"/>
|
||||
<path
|
||||
@@ -18,9 +18,16 @@
|
||||
android:width="24.0dp"
|
||||
android:height="24.0dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
|
||||
<path
|
||||
android:fillColor="#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:strokeColor="#FFFFFFFF"
|
||||
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>
|
||||
@@ -17,7 +17,8 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/ic_hourglass_top"
|
||||
/>
|
||||
<TextView
|
||||
android:id="@+id/digital_well_being_remaining_time"
|
||||
|
||||
@@ -29,5 +29,5 @@
|
||||
|
||||
<!-- Assistant Gesture -->
|
||||
<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>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
<dimen name="task_thumbnail_top_margin">24dp</dimen>
|
||||
<dimen name="task_thumbnail_half_top_margin">12dp</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_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_somewhat_fast">0.285dp</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 -->
|
||||
<dimen name="content_trans_y">50dp</dimen>
|
||||
@@ -63,8 +65,8 @@
|
||||
<dimen name="shelf_surface_offset">24dp</dimen>
|
||||
|
||||
<!-- Assistant Gestures -->
|
||||
<dimen name="gestures_assistant_size">28dp</dimen>
|
||||
<dimen name="gestures_assistant_drag_threshold">70dp</dimen>
|
||||
<dimen name="gestures_assistant_size">48dp</dimen>
|
||||
<dimen name="gestures_assistant_drag_threshold">55dp</dimen>
|
||||
|
||||
<!-- Distance to move elements when swiping up to go home from launcher -->
|
||||
<dimen name="home_pullback_distance">28dp</dimen>
|
||||
|
||||
@@ -58,4 +58,13 @@
|
||||
<!-- 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] -->
|
||||
<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>
|
||||
@@ -87,7 +87,7 @@ public class UiFactory extends RecentsUiFactory {
|
||||
OverviewInteractionState.INSTANCE.get(launcher)
|
||||
.setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */);
|
||||
if (launcher != null && launcher.getDragLayer() != null) {
|
||||
launcher.getDragLayer().setDisallowBackGesture(shouldBackButtonBeHidden);
|
||||
launcher.getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,5 @@ public class OverviewCallbacks implements ResourceBasedOverride {
|
||||
|
||||
public void onInitOverviewTransition() { }
|
||||
|
||||
public void onResetOverview() { }
|
||||
|
||||
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_REMOVED;
|
||||
|
||||
import static com.android.systemui.shared.system.PackageManagerWrapper
|
||||
.ACTION_PREFERRED_ACTIVITY_CHANGED;
|
||||
import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
|
||||
import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
@@ -29,7 +29,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.PatternMatcher;
|
||||
|
||||
import com.android.systemui.shared.system.PackageManagerWrapper;
|
||||
|
||||
@@ -109,13 +108,9 @@ public final class OverviewComponentObserver {
|
||||
}
|
||||
|
||||
mUpdateRegisteredPackage = defaultHome.getPackageName();
|
||||
IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED);
|
||||
updateReceiver.addAction(ACTION_PACKAGE_CHANGED);
|
||||
updateReceiver.addAction(ACTION_PACKAGE_REMOVED);
|
||||
updateReceiver.addDataScheme("package");
|
||||
updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage,
|
||||
PatternMatcher.PATTERN_LITERAL);
|
||||
mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver);
|
||||
mContext.registerReceiver(mOtherHomeAppUpdateReceiver, getPackageFilter(
|
||||
mUpdateRegisteredPackage, ACTION_PACKAGE_ADDED, ACTION_PACKAGE_CHANGED,
|
||||
ACTION_PACKAGE_REMOVED));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
|
||||
package com.android.quickstep;
|
||||
|
||||
import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.launcher3.util.MainThreadInitializedObject;
|
||||
import com.android.systemui.shared.system.QuickStepContract;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -70,8 +70,6 @@ public class SysUINavigationMode {
|
||||
mContext = context;
|
||||
initializeMode();
|
||||
|
||||
IntentFilter filter = new IntentFilter(ACTION_OVERLAY_CHANGED);
|
||||
filter.addDataScheme("package");
|
||||
mContext.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@@ -81,7 +79,7 @@ public class SysUINavigationMode {
|
||||
dispatchModeChange();
|
||||
}
|
||||
}
|
||||
}, filter);
|
||||
}, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
|
||||
}
|
||||
|
||||
private void initializeMode() {
|
||||
|
||||
@@ -111,6 +111,14 @@ public class TestInformationProvider extends ContentProvider {
|
||||
response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) distance);
|
||||
break;
|
||||
}
|
||||
|
||||
case TestProtocol.REQUEST_ENABLE_DRAG_LOGGING:
|
||||
TestProtocol.sDebugTracing = true;
|
||||
break;
|
||||
|
||||
case TestProtocol.REQUEST_DISABLE_DRAG_LOGGING:
|
||||
TestProtocol.sDebugTracing = false;
|
||||
break;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -36,17 +36,19 @@ import com.android.systemui.shared.system.MetricsLoggerCompat;
|
||||
@SuppressWarnings("unused")
|
||||
public class UserEventDispatcherExtension extends UserEventDispatcher {
|
||||
|
||||
public static final int ALL_APPS_PREDICTION_TIPS = 2;
|
||||
|
||||
private static final String TAG = "UserEventDispatcher";
|
||||
|
||||
public UserEventDispatcherExtension(Context context) { }
|
||||
|
||||
public void logStateChangeAction(int action, int dir, int srcChildTargetType,
|
||||
int srcParentContainerType, int dstContainerType,
|
||||
int pageIndex) {
|
||||
public void logStateChangeAction(int action, int dir, int downX, int downY,
|
||||
int srcChildTargetType, int srcParentContainerType,
|
||||
int dstContainerType, int pageIndex) {
|
||||
new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY,
|
||||
dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER);
|
||||
super.logStateChangeAction(action, dir, srcChildTargetType, srcParentContainerType,
|
||||
dstContainerType, pageIndex);
|
||||
super.logStateChangeAction(action, dir, downX, downY, srcChildTargetType,
|
||||
srcParentContainerType, dstContainerType, pageIndex);
|
||||
}
|
||||
|
||||
public void logActionTip(int actionType, int viewType) {
|
||||
|
||||
@@ -43,8 +43,6 @@ public class LayoutUtils {
|
||||
float extraSpace;
|
||||
if (dp.isVerticalBarLayout()) {
|
||||
extraSpace = 0;
|
||||
} else if (FeatureFlags.ENABLE_HINTS_IN_OVERVIEW.get()){
|
||||
extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx + dp.chipHintHeightPx;
|
||||
} else {
|
||||
extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx;
|
||||
}
|
||||
|
||||
@@ -38,28 +38,26 @@ public class MotionPauseDetector {
|
||||
private final float mSpeedVerySlow;
|
||||
private final float mSpeedSomewhatFast;
|
||||
private final float mSpeedFast;
|
||||
private final float mMinDisplacementForPause;
|
||||
private final Alarm mForcePauseTimeout;
|
||||
|
||||
private Long mPreviousTime = null;
|
||||
private Float mPreviousPosition = null;
|
||||
private Float mPreviousVelocity = null;
|
||||
|
||||
private TotalDisplacement mTotalDisplacement = new TotalDisplacement();
|
||||
private Float mFirstPosition = null;
|
||||
private Float mFirstOrthogonalPosition = null;
|
||||
|
||||
private OnMotionPauseListener mOnMotionPauseListener;
|
||||
private boolean mIsPaused;
|
||||
// Bias more for the first pause to make it feel extra responsive.
|
||||
private boolean mHasEverBeenPaused;
|
||||
/** @see #setDisallowPause(boolean) */
|
||||
private boolean mDisallowPause;
|
||||
|
||||
public MotionPauseDetector(Context context) {
|
||||
Resources res = context.getResources();
|
||||
mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
|
||||
mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
|
||||
mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
|
||||
mMinDisplacementForPause = res.getDimension(R.dimen.motion_pause_detector_min_displacement);
|
||||
mForcePauseTimeout = new Alarm();
|
||||
mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
|
||||
}
|
||||
@@ -72,29 +70,31 @@ public class MotionPauseDetector {
|
||||
mOnMotionPauseListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param disallowPause If true, we will not detect any pauses until this is set to false again.
|
||||
*/
|
||||
public void setDisallowPause(boolean disallowPause) {
|
||||
mDisallowPause = disallowPause;
|
||||
updatePaused(mIsPaused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes velocity and acceleration to determine whether the motion is paused.
|
||||
* @param position The x or y component of the motion being tracked.
|
||||
* @param orthogonalPosition The x or y component (opposite of {@param position}) of the motion.
|
||||
*
|
||||
* TODO: Use historical positions as well, e.g. {@link MotionEvent#getHistoricalY(int, int)}.
|
||||
*/
|
||||
public void addPosition(float position, float orthogonalPosition, long time) {
|
||||
public void addPosition(float position, long time) {
|
||||
if (mFirstPosition == null) {
|
||||
mFirstPosition = position;
|
||||
}
|
||||
if (mFirstOrthogonalPosition == null) {
|
||||
mFirstOrthogonalPosition = orthogonalPosition;
|
||||
}
|
||||
mForcePauseTimeout.setAlarm(FORCE_PAUSE_TIMEOUT);
|
||||
if (mPreviousTime != null && mPreviousPosition != null) {
|
||||
long changeInTime = Math.max(1, time - mPreviousTime);
|
||||
float changeInPosition = position - mPreviousPosition;
|
||||
float velocity = changeInPosition / changeInTime;
|
||||
if (mPreviousVelocity != null) {
|
||||
mTotalDisplacement.set(Math.abs(position - mFirstPosition),
|
||||
Math.abs(orthogonalPosition - mFirstOrthogonalPosition));
|
||||
checkMotionPaused(velocity, mPreviousVelocity, mTotalDisplacement);
|
||||
checkMotionPaused(velocity, mPreviousVelocity);
|
||||
}
|
||||
mPreviousVelocity = velocity;
|
||||
}
|
||||
@@ -102,8 +102,7 @@ public class MotionPauseDetector {
|
||||
mPreviousPosition = position;
|
||||
}
|
||||
|
||||
private void checkMotionPaused(float velocity, float prevVelocity,
|
||||
TotalDisplacement totalDisplacement) {
|
||||
private void checkMotionPaused(float velocity, float prevVelocity) {
|
||||
float speed = Math.abs(velocity);
|
||||
float previousSpeed = Math.abs(prevVelocity);
|
||||
boolean isPaused;
|
||||
@@ -125,16 +124,13 @@ public class MotionPauseDetector {
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean passedMinDisplacement = totalDisplacement.primary >= mMinDisplacementForPause;
|
||||
boolean isDisplacementOrthogonal = totalDisplacement.orthogonal > totalDisplacement.primary;
|
||||
if (!passedMinDisplacement || isDisplacementOrthogonal) {
|
||||
mForcePauseTimeout.cancelAlarm();
|
||||
isPaused = false;
|
||||
}
|
||||
updatePaused(isPaused);
|
||||
}
|
||||
|
||||
private void updatePaused(boolean isPaused) {
|
||||
if (mDisallowPause) {
|
||||
isPaused = false;
|
||||
}
|
||||
if (mIsPaused != isPaused) {
|
||||
mIsPaused = isPaused;
|
||||
if (mIsPaused) {
|
||||
@@ -151,8 +147,6 @@ public class MotionPauseDetector {
|
||||
mPreviousPosition = null;
|
||||
mPreviousVelocity = null;
|
||||
mFirstPosition = null;
|
||||
mFirstOrthogonalPosition = null;
|
||||
mTotalDisplacement.set(0, 0);
|
||||
setOnMotionPauseListener(null);
|
||||
mIsPaused = mHasEverBeenPaused = false;
|
||||
mForcePauseTimeout.cancelAlarm();
|
||||
@@ -165,18 +159,4 @@ public class MotionPauseDetector {
|
||||
public interface OnMotionPauseListener {
|
||||
void onMotionPauseChanged(boolean isPaused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the displacement from the first tracked position,
|
||||
* along both the primary and orthogonal axes.
|
||||
*/
|
||||
private class TotalDisplacement {
|
||||
public float primary;
|
||||
public float orthogonal;
|
||||
|
||||
public void set(float primaryDisplacement, float orthogonalDisplacement) {
|
||||
this.primary = primaryDisplacement;
|
||||
this.orthogonal = orthogonalDisplacement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.THREE_BUTTON;
|
||||
import static com.android.quickstep.NavigationModeSwitchRule.Mode.TWO_BUTTON;
|
||||
import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
|
||||
import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_2BUTTON_OVERLAY;
|
||||
import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_3BUTTON_OVERLAY;
|
||||
import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_GESTURAL_OVERLAY;
|
||||
@@ -85,9 +86,9 @@ public class NavigationModeSwitchRule implements TestRule {
|
||||
final LauncherInstrumentation.NavigationModel originalMode =
|
||||
mLauncher.getNavigationModel();
|
||||
try {
|
||||
// if (mode == ZERO_BUTTON || mode == ALL) {
|
||||
// evaluateWithZeroButtons();
|
||||
// }
|
||||
if (mode == ZERO_BUTTON || mode == ALL) {
|
||||
evaluateWithZeroButtons();
|
||||
}
|
||||
if (mode == TWO_BUTTON || mode == ALL) {
|
||||
evaluateWithTwoButtons();
|
||||
}
|
||||
@@ -131,7 +132,10 @@ public class NavigationModeSwitchRule implements TestRule {
|
||||
overlayPackage == NAV_BAR_MODE_GESTURAL_OVERLAY);
|
||||
|
||||
for (int i = 0; i != 100; ++i) {
|
||||
if (mLauncher.getNavigationModel() == expectedMode) return;
|
||||
if (mLauncher.getNavigationModel() == expectedMode) {
|
||||
Thread.sleep(1000);
|
||||
return;
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
Assert.fail("Couldn't switch to " + overlayPackage);
|
||||
|
||||
@@ -61,7 +61,7 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // Ignoring until gestural navigation event sequence settles
|
||||
@Ignore // Ignoring until race condition repro framework is changes for multi-process case.
|
||||
@NavigationModeSwitch(mode = Mode.TWO_BUTTON)
|
||||
public void testPressHome() {
|
||||
runTest(enterEvt(Launcher.ON_CREATE_EVT),
|
||||
@@ -75,14 +75,6 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest {
|
||||
exitEvt(Launcher.ON_CREATE_EVT));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // Ignoring until gestural navigation event sequence settles
|
||||
@NavigationModeSwitch(mode = Mode.TWO_BUTTON)
|
||||
public void testSwipeToOverview() {
|
||||
closeLauncherActivity();
|
||||
mLauncher.getBackground().switchToOverview();
|
||||
}
|
||||
|
||||
@Test
|
||||
@NavigationModeSwitch
|
||||
public void testStressPressHome() {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.quickstep;
|
||||
|
||||
import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -60,7 +62,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
|
||||
}
|
||||
|
||||
private void startTestApps() throws Exception {
|
||||
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_BROWSER));
|
||||
startAppFast(getAppPackageName());
|
||||
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
|
||||
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CONTACTS));
|
||||
|
||||
@@ -72,7 +74,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
|
||||
|
||||
@Test
|
||||
@PortraitLandscape
|
||||
@Ignore
|
||||
@Ignore // Enable after b/131115533
|
||||
public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException {
|
||||
mDevice.pressRecentApps();
|
||||
waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW);
|
||||
|
||||
@@ -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>
|
||||
@@ -17,8 +17,11 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorHint" >
|
||||
<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:fillColor="?android:attr/textColorHint"/>
|
||||
android:pathData="M20 6h-4V4c0-1.11-0.89-2-2-2h-4c-1.11 0-2 0.89-2 2v2H4c-1.11 0-1.99 0.89 -1.99
|
||||
2L2 19c0 1.11 0.89 2 2 2h16c1.11 0 2-0.89 2-2V8c0-1.11-0.89-2-2-2zM10
|
||||
4h4v2h-4V4zm10 15H4V8h16v11z"
|
||||
android:fillColor="@android:color/white"/>
|
||||
</vector>
|
||||
@@ -22,7 +22,6 @@
|
||||
android:tint="?android:attr/textColorHint" >
|
||||
|
||||
<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,
|
||||
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:pathData="M20,9H4v2h16V9z M4,15h16v-2H4V15z"
|
||||
android:fillColor="@android:color/white" />
|
||||
</vector>
|
||||
@@ -21,12 +21,12 @@
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M 11 7 H 13 V 9 H 11 V 7 Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M 11 11 H 13 V 17 H 11 V 11 Z" />
|
||||
<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" />
|
||||
</vector>
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorPrimary"
|
||||
android:pathData="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" />
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z" />
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
@@ -14,14 +14,12 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorPrimary" >
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
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,
|
||||
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"/>
|
||||
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>
|
||||
|
||||
@@ -20,7 +20,7 @@ Copyright (C) 2016 The Android Open Source Project
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorPrimary" >
|
||||
<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
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
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"/>
|
||||
</vector>
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorPrimary" >
|
||||
<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" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M 9 8 H 11 V 17 H 9 V 8 Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M 13 8 H 15 V 17 H 13 V 8 Z" />
|
||||
</vector>
|
||||
|
||||
@@ -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");
|
||||
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:height="@dimen/options_menu_icon_size"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
<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
|
||||
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"/>
|
||||
|
||||
@@ -20,8 +20,12 @@
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorPrimary" >
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
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
|
||||
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
|
||||
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="M12,5.99L19.53,19H4.47L12,5.99 M12,2L1,21h22L12,2L12,2z" />
|
||||
<path
|
||||
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>
|
||||
|
||||
@@ -17,9 +17,10 @@ Copyright (C) 2016 The Android Open Source Project
|
||||
android:width="@dimen/options_menu_icon_size"
|
||||
android:height="@dimen/options_menu_icon_size"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
<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
|
||||
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>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
android:clipChildren="true"
|
||||
android:clipToPadding="false"
|
||||
android:focusable="false"
|
||||
android:saveEnabled="false" >
|
||||
android:saveEnabled="false">
|
||||
|
||||
<include
|
||||
layout="@layout/all_apps_rv_layout"
|
||||
@@ -47,7 +47,8 @@
|
||||
android:layout_height="@dimen/all_apps_header_tab_height"
|
||||
android:layout_marginLeft="@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
|
||||
android:id="@+id/tab_personal"
|
||||
@@ -55,7 +56,6 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/all_apps_personal_tab"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/all_apps_tab_text"
|
||||
@@ -67,7 +67,6 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/all_apps_work_tab"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/all_apps_tab_text"
|
||||
|
||||
@@ -29,11 +29,10 @@
|
||||
android:textAlignment="viewStart"
|
||||
android:paddingStart="@dimen/deep_shortcuts_text_padding_start"
|
||||
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:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:fontFamily="sans-serif"
|
||||
launcher:layoutHorizontal="true"
|
||||
launcher:iconDisplay="shortcut_popup"
|
||||
launcher:iconSizeOverride="@dimen/deep_shortcut_icon_size" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user