callbacks) {
+ super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+ }
+
+ @Override
+ public void bindDeepShortcuts() {
+ }
+
+ @Override
+ public void bindWidgets() {
+ }
+}
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..18f3f9d91ce79ae72d8102ecf50cdb63c78124b1
--- /dev/null
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+package com.android.launcher3.model;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.WidgetListRowEntry;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Widgets data model that is used by the adapters of the widget views and controllers.
+ *
+ * The widgets and shortcuts are organized using package name as its index.
+ */
+public class WidgetsModel {
+ private static final ArrayList EMPTY_WIDGET_LIST = new ArrayList<>();
+
+ /**
+ * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row
+ * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s
+ * is not sorted. This list is sorted at the UI when using
+ * {@link com.android.launcher3.widget.WidgetsDiffReporter}
+ *
+ * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList)
+ */
+ public synchronized ArrayList getWidgetsList(Context context) {
+ return EMPTY_WIDGET_LIST;
+ }
+
+ /**
+ * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
+ * only widgets and shortcuts associated with the package/user are.
+ */
+ public List update(LauncherAppState app,
+ @Nullable PackageUserKey packageUser) {
+ return Collections.emptyList();
+ }
+
+
+ public void onPackageIconsUpdated(Set packageNames, UserHandle user,
+ LauncherAppState app) {
+ }
+}
\ No newline at end of file
diff --git a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e449108d8598c4c43cadacd16f8bd790e851c96
--- /dev/null
+++ b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+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;
+import android.os.UserHandle;
+
+import com.android.launcher3.ItemInfo;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
+ */
+public class DeepShortcutManager {
+ private static DeepShortcutManager sInstance;
+ private static final Object sInstanceLock = new Object();
+
+ public static DeepShortcutManager getInstance(Context context) {
+ synchronized (sInstanceLock) {
+ if (sInstance == null) {
+ sInstance = new DeepShortcutManager(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+ }
+
+ private DeepShortcutManager(Context context) {
+ }
+
+ public static boolean supportsShortcuts(ItemInfo info) {
+ return false;
+ }
+
+ public boolean wasLastCallSuccess() {
+ return false;
+ }
+
+ public void onShortcutsChanged(List shortcuts) {
+ }
+
+ /**
+ * Queries for the shortcuts with the package name and provided ids.
+ *
+ * This method is intended to get the full details for shortcuts when they are added or updated,
+ * because we only get "key" fields in onShortcutsChanged().
+ */
+ public List queryForFullDetails(String packageName,
+ List shortcutIds, UserHandle user) {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Gets all the manifest and dynamic shortcuts associated with the given package and user,
+ * to be displayed in the shortcuts container on long press.
+ */
+ public List queryForShortcutsContainer(ComponentName activity,
+ UserHandle user) {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Removes the given shortcut from the current list of pinned shortcuts.
+ * (Runs on background thread)
+ */
+ public void unpinShortcut(final ShortcutKey key) {
+ }
+
+ /**
+ * Adds the given shortcut to the current list of pinned shortcuts.
+ * (Runs on background thread)
+ */
+ public void pinShortcut(final ShortcutKey key) {
+ }
+
+ public void startShortcut(String packageName, String id, Rect sourceBounds,
+ Bundle startActivityOptions, UserHandle user) {
+ }
+
+ public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
+ return null;
+ }
+
+ /**
+ * Returns the id's of pinned shortcuts associated with the given package and user.
+ *
+ * If packageName is null, returns all pinned shortcuts regardless of package.
+ */
+ public List queryForPinnedShortcuts(String packageName, UserHandle user) {
+ return Collections.emptyList();
+ }
+
+ public List queryForPinnedShortcuts(String packageName,
+ List shortcutIds, UserHandle user) {
+ return Collections.emptyList();
+ }
+
+ public List queryForAllShortcuts(UserHandle user) {
+ return Collections.emptyList();
+ }
+
+ public boolean hasHostPermission() {
+ return false;
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..5b90f08148fd64eced95f041b263732232ffc0d3
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,13 @@
+# Until all the dependencies move to android X
+android.useAndroidX = true
+android.enableJetifier = true
+
+ANDROID_X_VERSION=1.0.0-beta01
+
+GRADLE_CLASS_PATH=com.android.tools.build:gradle:3.3.0
+
+PROTOBUF_CLASS_PATH=com.google.protobuf:protobuf-gradle-plugin:0.8.6
+PROTOBUF_DEPENDENCY=com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7
+
+BUILD_TOOLS_VERSION=28.0.3
+COMPILE_SDK=android-Q
\ No newline at end of file
diff --git a/iconloaderlib/Android.bp b/iconloaderlib/Android.bp
new file mode 100644
index 0000000000000000000000000000000000000000..f12d16e42a03ade46f203c4dfa8a0c15425d91a3
--- /dev/null
+++ b/iconloaderlib/Android.bp
@@ -0,0 +1,44 @@
+// 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.
+
+android_library {
+ name: "iconloader_base",
+ sdk_version: "28",
+ min_sdk_version: "21",
+ static_libs: [
+ "androidx.core_core",
+ ],
+ resource_dirs: [
+ "res",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+}
+
+android_library {
+ name: "iconloader",
+ sdk_version: "system_current",
+ min_sdk_version: "21",
+ static_libs: [
+ "androidx.core_core",
+ ],
+ resource_dirs: [
+ "res",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "src_full_lib/**/*.java",
+ ],
+}
diff --git a/res/drawable/all_apps_button_icon.xml b/iconloaderlib/AndroidManifest.xml
similarity index 62%
rename from res/drawable/all_apps_button_icon.xml
rename to iconloaderlib/AndroidManifest.xml
index 7c69cad3ac498235fb996b9665bdf705e1bc0b44..b30258da2a30956e9f8bb0046d3bc33740d2d3d4 100644
--- a/res/drawable/all_apps_button_icon.xml
+++ b/iconloaderlib/AndroidManifest.xml
@@ -1,5 +1,6 @@
-
-
-
-
-
-
+
+
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..8a4d2b76e56cc499295cf808ac9d1cfaf2dadbb6
--- /dev/null
+++ b/iconloaderlib/build.gradle
@@ -0,0 +1,39 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion COMPILE_SDK
+ buildToolsVersion BUILD_TOOLS_VERSION
+ publishNonDefault true
+
+ defaultConfig {
+ minSdkVersion 25
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src', 'src_full_lib']
+ manifest.srcFile 'AndroidManifest.xml'
+ res.srcDirs = ['res']
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ tasks.withType(JavaCompile) {
+ options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation "androidx.core:core:${ANDROID_X_VERSION}"
+}
diff --git a/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
similarity index 93%
rename from res/drawable-v26/adaptive_icon_drawable_wrapper.xml
rename to iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
index 2d78b699f4f6e369641983da424ee4cf0c97a84d..9f13cf57196f5985b338f8c290a62b10859d1105 100644
--- a/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
+++ b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/res/drawable/ic_instant_app_badge.xml b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
similarity index 93%
rename from res/drawable/ic_instant_app_badge.xml
rename to iconloaderlib/res/drawable/ic_instant_app_badge.xml
index cc532309c96fefd589259c48ef4e3095ccdcfe51..b74317e5f2be33a41de4823d960f14079f15879a 100644
--- a/res/drawable/ic_instant_app_badge.xml
+++ b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
@@ -21,23 +21,19 @@
-
\ No newline at end of file
+
diff --git a/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml
new file mode 100644
index 0000000000000000000000000000000000000000..873b2fc5fb544828ff1fd6a1f9be55eb9d87deeb
--- /dev/null
+++ b/iconloaderlib/res/values/colors.xml
@@ -0,0 +1,21 @@
+
+
+
+ #FFFFFF
+
diff --git a/iconloaderlib/res/values/config.xml b/iconloaderlib/res/values/config.xml
new file mode 100644
index 0000000000000000000000000000000000000000..68c2d2e3aa0617f54aa2a6ff001cfa1e6a49ff98
--- /dev/null
+++ b/iconloaderlib/res/values/config.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ 56dp
+ false
+ app_icons.db
+
+
\ No newline at end of file
diff --git a/iconloaderlib/res/values/dimens.xml b/iconloaderlib/res/values/dimens.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e8c0c44f72199e6f354ee5d57acd67ccbef14bc6
--- /dev/null
+++ b/iconloaderlib/res/values/dimens.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ 24dp
+
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..60320d63be080da5c84d4ca1ddb149ed800de754
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -0,0 +1,351 @@
+package com.android.launcher3.icons;
+
+import static android.graphics.Paint.DITHER_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
+import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
+
+/**
+ * This class will be moved to androidx library. There shouldn't be any dependency outside
+ * this package.
+ */
+public class BaseIconFactory implements AutoCloseable {
+
+ private static final String TAG = "BaseIconFactory";
+ private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
+ static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+ static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+
+ private final Rect mOldBounds = new Rect();
+ protected final Context mContext;
+ private final Canvas mCanvas;
+ private final PackageManager mPm;
+ private final ColorExtractor mColorExtractor;
+ private boolean mDisableColorExtractor;
+
+ protected final int mFillResIconDpi;
+ protected final int mIconBitmapSize;
+
+ private IconNormalizer mNormalizer;
+ private ShadowGenerator mShadowGenerator;
+ private final boolean mShapeDetection;
+
+ private Drawable mWrapperIcon;
+ private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
+
+ protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
+ boolean shapeDetection) {
+ mContext = context.getApplicationContext();
+ mShapeDetection = shapeDetection;
+ mFillResIconDpi = fillResIconDpi;
+ mIconBitmapSize = iconBitmapSize;
+
+ mPm = mContext.getPackageManager();
+ mColorExtractor = new ColorExtractor();
+
+ mCanvas = new Canvas();
+ mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
+ clear();
+ }
+
+ protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
+ this(context, fillResIconDpi, iconBitmapSize, false);
+ }
+
+ protected void clear() {
+ mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
+ mDisableColorExtractor = false;
+ }
+
+ public ShadowGenerator getShadowGenerator() {
+ if (mShadowGenerator == null) {
+ mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
+ }
+ return mShadowGenerator;
+ }
+
+ public IconNormalizer getNormalizer() {
+ if (mNormalizer == null) {
+ mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
+ }
+ return mNormalizer;
+ }
+
+ @SuppressWarnings("deprecation")
+ public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) {
+ try {
+ Resources resources = mPm.getResourcesForApplication(iconRes.packageName);
+ if (resources != null) {
+ final int id = resources.getIdentifier(iconRes.resourceName, null, null);
+ // do not stamp old legacy shortcuts as the app may have already forgotten about it
+ return createBadgedIconBitmap(
+ resources.getDrawableForDensity(id, mFillResIconDpi),
+ Process.myUserHandle() /* only available on primary user */,
+ false /* do not apply legacy treatment */);
+ }
+ } catch (Exception e) {
+ // Icon not found.
+ }
+ return null;
+ }
+
+ public BitmapInfo createIconBitmap(Bitmap icon) {
+ if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
+ icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
+ }
+
+ return BitmapInfo.fromBitmap(icon, mDisableColorExtractor ? null : mColorExtractor);
+ }
+
+ public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+ boolean shrinkNonAdaptiveIcons) {
+ return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null);
+ }
+
+ public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+ int iconAppTargetSdk) {
+ return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
+ }
+
+ public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+ int iconAppTargetSdk, boolean isInstantApp) {
+ return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null);
+ }
+
+ public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+ int iconAppTargetSdk, boolean isInstantApp, float[] scale) {
+ boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
+ (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
+ return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale);
+ }
+
+ public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) {
+ boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
+ (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
+ return createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons);
+ }
+
+ /**
+ * Creates bitmap using the source drawable and various parameters.
+ * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
+ *
+ * @param icon source of the icon
+ * @param user info can be used for a badge
+ * @param shrinkNonAdaptiveIcons {@code true} if non adaptive icons should be treated
+ * @param isInstantApp info can be used for a badge
+ * @param scale returns the scale result from normalization
+ * @return a bitmap suitable for disaplaying as an icon at various system UIs.
+ */
+ public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+ boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) {
+ if (scale == null) {
+ scale = new float[1];
+ }
+ icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
+ Bitmap bitmap = createIconBitmap(icon, scale[0]);
+ if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
+ mCanvas.setBitmap(bitmap);
+ getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
+ mCanvas.setBitmap(null);
+ }
+
+ if (isInstantApp) {
+ badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge));
+ }
+ if (user != null) {
+ BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
+ Drawable badged = mPm.getUserBadgedIcon(drawable, user);
+ if (badged instanceof BitmapDrawable) {
+ bitmap = ((BitmapDrawable) badged).getBitmap();
+ } else {
+ bitmap = createIconBitmap(badged, 1f);
+ }
+ }
+ return BitmapInfo.fromBitmap(bitmap, mDisableColorExtractor ? null : mColorExtractor);
+ }
+
+ public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
+ RectF iconBounds = new RectF();
+ float[] scale = new float[1];
+ icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale);
+ return createIconBitmap(icon,
+ Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)));
+ }
+
+ /**
+ * Sets the background color used for wrapped adaptive icon
+ */
+ public void setWrapperBackgroundColor(int color) {
+ mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
+ }
+
+ /**
+ * Disables the dominant color extraction for all icons loaded.
+ */
+ public void disableColorExtraction() {
+ mDisableColorExtractor = true;
+ }
+
+ private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, boolean shrinkNonAdaptiveIcons,
+ RectF outIconBounds, float[] outScale) {
+ float scale = 1f;
+
+ if (shrinkNonAdaptiveIcons && ATLEAST_OREO) {
+ if (mWrapperIcon == null) {
+ mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
+ .mutate();
+ }
+ AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
+ dr.setBounds(0, 0, 1, 1);
+ boolean[] outShape = new boolean[1];
+ scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
+ if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) {
+ FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
+ fsd.setDrawable(icon);
+ fsd.setScale(scale);
+ icon = dr;
+ scale = getNormalizer().getScale(icon, outIconBounds, null, null);
+
+ ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
+ }
+ } else {
+ scale = getNormalizer().getScale(icon, outIconBounds, null, null);
+ }
+
+ outScale[0] = scale;
+ return icon;
+ }
+
+ /**
+ * Adds the {@param badge} on top of {@param target} using the badge dimensions.
+ */
+ public void badgeWithDrawable(Bitmap target, Drawable badge) {
+ mCanvas.setBitmap(target);
+ badgeWithDrawable(mCanvas, badge);
+ mCanvas.setBitmap(null);
+ }
+
+ /**
+ * Adds the {@param badge} on top of {@param target} using the badge dimensions.
+ */
+ public void badgeWithDrawable(Canvas target, Drawable badge) {
+ int badgeSize = mContext.getResources().getDimensionPixelSize(R.dimen.profile_badge_size);
+ badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize,
+ mIconBitmapSize, mIconBitmapSize);
+ badge.draw(target);
+ }
+
+ private Bitmap createIconBitmap(Drawable icon, float scale) {
+ return createIconBitmap(icon, scale, mIconBitmapSize);
+ }
+
+ /**
+ * @param icon drawable that should be flattened to a bitmap
+ * @param scale the scale to apply before drawing {@param icon} on the canvas
+ */
+ public Bitmap createIconBitmap(Drawable icon, float scale, int size) {
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ if (icon == null) {
+ return bitmap;
+ }
+ mCanvas.setBitmap(bitmap);
+ mOldBounds.set(icon.getBounds());
+
+ if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
+ int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
+ Math.round(size * (1 - scale) / 2 ));
+ icon.setBounds(offset, offset, size - offset, size - offset);
+ icon.draw(mCanvas);
+ } else {
+ if (icon instanceof BitmapDrawable) {
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
+ Bitmap b = bitmapDrawable.getBitmap();
+ if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
+ bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
+ }
+ }
+ int width = size;
+ int height = size;
+
+ int intrinsicWidth = icon.getIntrinsicWidth();
+ int intrinsicHeight = icon.getIntrinsicHeight();
+ if (intrinsicWidth > 0 && intrinsicHeight > 0) {
+ // Scale the icon proportionally to the icon dimensions
+ final float ratio = (float) intrinsicWidth / intrinsicHeight;
+ if (intrinsicWidth > intrinsicHeight) {
+ height = (int) (width / ratio);
+ } else if (intrinsicHeight > intrinsicWidth) {
+ width = (int) (height * ratio);
+ }
+ }
+ final int left = (size - width) / 2;
+ final int top = (size - height) / 2;
+ icon.setBounds(left, top, left + width, top + height);
+ mCanvas.save();
+ mCanvas.scale(scale, scale, size / 2, size / 2);
+ icon.draw(mCanvas);
+ mCanvas.restore();
+
+ }
+ icon.setBounds(mOldBounds);
+ mCanvas.setBitmap(null);
+ return bitmap;
+ }
+
+ @Override
+ public void close() {
+ clear();
+ }
+
+ public BitmapInfo makeDefaultIcon(UserHandle user) {
+ return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi),
+ user, Build.VERSION.SDK_INT);
+ }
+
+ public static Drawable getFullResDefaultActivityIcon(int iconDpi) {
+ return Resources.getSystem().getDrawableForDensity(
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+ ? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon,
+ iconDpi);
+ }
+
+ /**
+ * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
+ * This allows the badging to be done based on the action bitmap size rather than
+ * the scaled bitmap size.
+ */
+ private static class FixedSizeBitmapDrawable extends BitmapDrawable {
+
+ public FixedSizeBitmapDrawable(Bitmap bitmap) {
+ super(null, bitmap);
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return getBitmap().getWidth();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return getBitmap().getWidth();
+ }
+ }
+}
diff --git a/src/com/android/launcher3/graphics/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
similarity index 65%
rename from src/com/android/launcher3/graphics/BitmapInfo.java
rename to iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index ab906e2f0944b263fdc56bf3765c0e4efc9c24d0..245561ea5376acd58cd4b621035d1c8cddc066f1 100644
--- a/src/com/android/launcher3/graphics/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -13,31 +13,37 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.launcher3.graphics;
+package com.android.launcher3.icons;
import android.graphics.Bitmap;
-
-import com.android.launcher3.ItemInfoWithIcon;
+import android.graphics.Bitmap.Config;
public class BitmapInfo {
+ public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
+
public Bitmap icon;
public int color;
- public void applyTo(ItemInfoWithIcon info) {
- info.iconBitmap = icon;
- info.iconColor = color;
- }
-
public void applyTo(BitmapInfo info) {
info.icon = icon;
info.color = color;
}
+ public final boolean isLowRes() {
+ return LOW_RES_ICON == icon;
+ }
+
public static BitmapInfo fromBitmap(Bitmap bitmap) {
+ return fromBitmap(bitmap, null);
+ }
+
+ public static BitmapInfo fromBitmap(Bitmap bitmap, ColorExtractor dominantColorExtractor) {
BitmapInfo info = new BitmapInfo();
info.icon = bitmap;
- info.color = ColorExtractor.findDominantColorByHue(bitmap);
+ info.color = dominantColorExtractor != null
+ ? dominantColorExtractor.findDominantColorByHue(bitmap)
+ : 0;
return info;
}
}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..a66b929efbd5ebf95e8a46309834a7e31f00df25
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.icons;
+
+import android.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Picture;
+import android.os.Build;
+
+/**
+ * Interface representing a bitmap draw operation.
+ */
+public interface BitmapRenderer {
+
+ boolean USE_HARDWARE_BITMAP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+
+ static Bitmap createSoftwareBitmap(int width, int height, BitmapRenderer renderer) {
+ Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ renderer.draw(new Canvas(result));
+ return result;
+ }
+
+ @TargetApi(Build.VERSION_CODES.P)
+ static Bitmap createHardwareBitmap(int width, int height, BitmapRenderer renderer) {
+ if (!USE_HARDWARE_BITMAP) {
+ return createSoftwareBitmap(width, height, renderer);
+ }
+
+ Picture picture = new Picture();
+ renderer.draw(picture.beginRecording(width, height));
+ picture.endRecording();
+ return Bitmap.createBitmap(picture);
+ }
+
+ void draw(Canvas out);
+}
diff --git a/src/com/android/launcher3/graphics/ColorExtractor.java b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
similarity index 79%
rename from src/com/android/launcher3/graphics/ColorExtractor.java
rename to iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
index e9d72b7931d46d9a468bcce549b10d51b431cd0c..87bda825cc6d4f4d669d3356be57e7895ca40a8a 100644
--- a/src/com/android/launcher3/graphics/ColorExtractor.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
@@ -13,27 +13,37 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.launcher3.graphics;
+package com.android.launcher3.icons;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.util.SparseArray;
+import java.util.Arrays;
/**
* Utility class for extracting colors from a bitmap.
*/
public class ColorExtractor {
- public static int findDominantColorByHue(Bitmap bitmap) {
- return findDominantColorByHue(bitmap, 20);
+ private final int NUM_SAMPLES = 20;
+ private final float[] mTmpHsv = new float[3];
+ private final float[] mTmpHueScoreHistogram = new float[360];
+ private final int[] mTmpPixels = new int[NUM_SAMPLES];
+ private final SparseArray mTmpRgbScores = new SparseArray<>();
+
+ /**
+ * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
+ * @param bitmap The bitmap to scan
+ */
+ public int findDominantColorByHue(Bitmap bitmap) {
+ return findDominantColorByHue(bitmap, NUM_SAMPLES);
}
/**
* This picks a dominant color, looking for high-saturation, high-value, repeated hues.
* @param bitmap The bitmap to scan
- * @param samples The approximate max number of samples to use.
*/
- public static int findDominantColorByHue(Bitmap bitmap, int samples) {
+ public int findDominantColorByHue(Bitmap bitmap, int samples) {
final int height = bitmap.getHeight();
final int width = bitmap.getWidth();
int sampleStride = (int) Math.sqrt((height * width) / samples);
@@ -42,15 +52,18 @@ public class ColorExtractor {
}
// This is an out-param, for getting the hsv values for an rgb
- float[] hsv = new float[3];
+ float[] hsv = mTmpHsv;
+ Arrays.fill(hsv, 0);
// First get the best hue, by creating a histogram over 360 hue buckets,
// where each pixel contributes a score weighted by saturation, value, and alpha.
- float[] hueScoreHistogram = new float[360];
+ float[] hueScoreHistogram = mTmpHueScoreHistogram;
+ Arrays.fill(hueScoreHistogram, 0);
float highScore = -1;
int bestHue = -1;
- int[] pixels = new int[samples];
+ int[] pixels = mTmpPixels;
+ Arrays.fill(pixels, 0);
int pixelCount = 0;
for (int y = 0; y < height; y += sampleStride) {
@@ -82,7 +95,8 @@ public class ColorExtractor {
}
}
- SparseArray rgbScores = new SparseArray<>();
+ SparseArray rgbScores = mTmpRgbScores;
+ rgbScores.clear();
int bestColor = 0xff000000;
highScore = -1;
// Go back over the RGB colors that match the winning hue,
diff --git a/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..97a0fd3ffca69988b42fc25456a39c15b715804f
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+package com.android.launcher3.icons;
+
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.ViewDebug;
+
+/**
+ * Used to draw a notification dot on top of an icon.
+ */
+public class DotRenderer {
+
+ private static final String TAG = "DotRenderer";
+
+ // The dot size is defined as a percentage of the app icon size.
+ private static final float SIZE_PERCENTAGE = 0.228f;
+
+ private final float mCircleRadius;
+ private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
+
+ private final Bitmap mBackgroundWithShadow;
+ private final float mBitmapOffset;
+
+ // Stores the center x and y position as a percentage (0 to 1) of the icon size
+ private final float[] mRightDotPosition;
+ private final float[] mLeftDotPosition;
+
+ public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) {
+ int size = Math.round(SIZE_PERCENTAGE * iconSizePx);
+ ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT);
+ builder.ambientShadowAlpha = 88;
+ mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size);
+ mCircleRadius = builder.radius;
+
+ mBitmapOffset = -mBackgroundWithShadow.getHeight() * 0.5f; // Same as width.
+
+ // Find the points on the path that are closest to the top left and right corners.
+ mLeftDotPosition = getPathPoint(iconShapePath, pathSize, -1);
+ mRightDotPosition = getPathPoint(iconShapePath, pathSize, 1);
+ }
+
+ private static float[] getPathPoint(Path path, float size, float direction) {
+ float halfSize = size / 2;
+ // Small delta so that we don't get a zero size triangle
+ float delta = 1;
+
+ float x = halfSize + direction * halfSize;
+ Path trianglePath = new Path();
+ trianglePath.moveTo(halfSize, halfSize);
+ trianglePath.lineTo(x + delta * direction, 0);
+ trianglePath.lineTo(x, -delta);
+ trianglePath.close();
+
+ trianglePath.op(path, Path.Op.INTERSECT);
+ float[] pos = new float[2];
+ new PathMeasure(trianglePath, false).getPosTan(0, pos, null);
+
+ pos[0] = pos[0] / size;
+ pos[1] = pos[1] / size;
+ return pos;
+ }
+
+ public float[] getLeftDotPosition() {
+ return mLeftDotPosition;
+ }
+
+ public float[] getRightDotPosition() {
+ return mRightDotPosition;
+ }
+
+ /**
+ * Draw a circle on top of the canvas according to the given params.
+ */
+ public void draw(Canvas canvas, DrawParams params) {
+ if (params == null) {
+ Log.e(TAG, "Invalid null argument(s) passed in call to draw.");
+ return;
+ }
+ canvas.save();
+
+ Rect iconBounds = params.iconBounds;
+ float[] dotPosition = params.leftAlign ? mLeftDotPosition : mRightDotPosition;
+ float dotCenterX = iconBounds.left + iconBounds.width() * dotPosition[0];
+ float dotCenterY = iconBounds.top + iconBounds.height() * dotPosition[1];
+
+ // Ensure dot fits entirely in canvas clip bounds.
+ Rect canvasBounds = canvas.getClipBounds();
+ float offsetX = params.leftAlign
+ ? Math.max(0, canvasBounds.left - (dotCenterX + mBitmapOffset))
+ : Math.min(0, canvasBounds.right - (dotCenterX - mBitmapOffset));
+ float offsetY = Math.max(0, canvasBounds.top - (dotCenterY + mBitmapOffset));
+
+ // We draw the dot relative to its center.
+ canvas.translate(dotCenterX + offsetX, dotCenterY + offsetY);
+ canvas.scale(params.scale, params.scale);
+
+ mCirclePaint.setColor(Color.BLACK);
+ canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint);
+ mCirclePaint.setColor(params.color);
+ canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint);
+ canvas.restore();
+ }
+
+ public static class DrawParams {
+ /** The color (possibly based on the icon) to use for the dot. */
+ @ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true)
+ public int color;
+ /** The bounds of the icon that the dot is drawn on top of. */
+ @ViewDebug.ExportedProperty(category = "notification dot")
+ public Rect iconBounds = new Rect();
+ /** The progress of the animation, from 0 to 1. */
+ @ViewDebug.ExportedProperty(category = "notification dot")
+ public float scale;
+ /** Whether the dot should align to the top left of the icon rather than the top right. */
+ @ViewDebug.ExportedProperty(category = "notification dot")
+ public boolean leftAlign;
+ }
+}
diff --git a/src/com/android/launcher3/graphics/FixedScaleDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java
similarity index 92%
rename from src/com/android/launcher3/graphics/FixedScaleDrawable.java
rename to iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java
index 0f0e42428d525a6afac19535501122b2b3a3a499..516965ec2ba2fb6f30723e030a9b4f58fed33fe4 100644
--- a/src/com/android/launcher3/graphics/FixedScaleDrawable.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java
@@ -1,12 +1,10 @@
-package com.android.launcher3.graphics;
+package com.android.launcher3.icons;
-import android.annotation.TargetApi;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.graphics.Canvas;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.DrawableWrapper;
-import android.os.Build;
import android.util.AttributeSet;
import org.xmlpull.v1.XmlPullParser;
@@ -14,7 +12,6 @@ import org.xmlpull.v1.XmlPullParser;
/**
* Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
*/
-@TargetApi(Build.VERSION_CODES.N)
public class FixedScaleDrawable extends DrawableWrapper {
// TODO b/33553066 use the constant defined in MaskableIconDrawable
diff --git a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..3e818a5568f4ee4c9f878825b0d3d367ed1a2882
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.icons;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import androidx.annotation.ColorInt;
+
+public class GraphicsUtils {
+
+ private static final String TAG = "GraphicsUtils";
+
+ /**
+ * Set the alpha component of {@code color} to be {@code alpha}. Unlike the support lib version,
+ * it bounds the alpha in valid range instead of throwing an exception to allow for safer
+ * interpolation of color animations
+ */
+ @ColorInt
+ public static int setColorAlphaBound(int color, int alpha) {
+ if (alpha < 0) {
+ alpha = 0;
+ } else if (alpha > 255) {
+ alpha = 255;
+ }
+ return (color & 0x00ffffff) | (alpha << 24);
+ }
+
+ /**
+ * Compresses the bitmap to a byte array for serialization.
+ */
+ public static byte[] flattenBitmap(Bitmap bitmap) {
+ // Try go guesstimate how much space the icon will take when serialized
+ // to avoid unnecessary allocations/copies during the write (4 bytes per pixel).
+ int size = bitmap.getWidth() * bitmap.getHeight() * 4;
+ ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+ try {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.flush();
+ out.close();
+ return out.toByteArray();
+ } catch (IOException e) {
+ Log.w(TAG, "Could not write bitmap");
+ return null;
+ }
+ }
+
+ public static int getArea(Region r) {
+ RegionIterator itr = new RegionIterator(r);
+ int area = 0;
+ Rect tempRect = new Rect();
+ while (itr.next(tempRect)) {
+ area += tempRect.width() * tempRect.height();
+ }
+ return area;
+ }
+}
diff --git a/src/com/android/launcher3/graphics/IconNormalizer.java b/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java
similarity index 84%
rename from src/com/android/launcher3/graphics/IconNormalizer.java
rename to iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java
index a2a08015e3e1c6e187536922a7d4dbc2d7c7bed7..de39e79fec02bc13129a437465539d3887d18ea4 100644
--- a/src/com/android/launcher3/graphics/IconNormalizer.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java
@@ -14,9 +14,11 @@
* limitations under the License.
*/
-package com.android.launcher3.graphics;
+package com.android.launcher3.icons;
+import android.annotation.TargetApi;
import android.content.Context;
+import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -24,22 +26,20 @@ import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
-import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.Region;
import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import android.os.Build;
import android.util.Log;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
import java.nio.ByteBuffer;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
public class IconNormalizer {
private static final String TAG = "IconNormalizer";
@@ -72,9 +72,11 @@ public class IconNormalizer {
private final Paint mPaintMaskShapeOutline;
private final byte[] mPixels;
- private final Rect mAdaptiveIconBounds;
+ private final RectF mAdaptiveIconBounds;
private float mAdaptiveIconScale;
+ private boolean mEnableShapeDetection;
+
// for each y, stores the position of the leftmost x and the rightmost x
private final float[] mLeftBorder;
private final float[] mRightBorder;
@@ -83,16 +85,16 @@ public class IconNormalizer {
private final Matrix mMatrix;
/** package private **/
- IconNormalizer(Context context) {
+ IconNormalizer(Context context, int iconBitmapSize, boolean shapeDetection) {
// Use twice the icon size as maximum size to avoid scaling down twice.
- mMaxSize = LauncherAppState.getIDP(context).iconBitmapSize * 2;
+ mMaxSize = iconBitmapSize * 2;
mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
mCanvas = new Canvas(mBitmap);
mPixels = new byte[mMaxSize * mMaxSize];
mLeftBorder = new float[mMaxSize];
mRightBorder = new float[mMaxSize];
mBounds = new Rect();
- mAdaptiveIconBounds = new Rect();
+ mAdaptiveIconBounds = new RectF();
mPaintMaskShape = new Paint();
mPaintMaskShape.setColor(Color.RED);
@@ -100,7 +102,8 @@ public class IconNormalizer {
mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
mPaintMaskShapeOutline = new Paint();
- mPaintMaskShapeOutline.setStrokeWidth(2 * context.getResources().getDisplayMetrics().density);
+ mPaintMaskShapeOutline.setStrokeWidth(
+ 2 * context.getResources().getDisplayMetrics().density);
mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE);
mPaintMaskShapeOutline.setColor(Color.BLACK);
mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
@@ -108,6 +111,49 @@ public class IconNormalizer {
mShapePath = new Path();
mMatrix = new Matrix();
mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
+ mEnableShapeDetection = shapeDetection;
+ }
+
+ private static float getScale(float hullArea, float boundingArea, float fullArea) {
+ float hullByRect = hullArea / boundingArea;
+ float scaleRequired;
+ if (hullByRect < CIRCLE_AREA_BY_RECT) {
+ scaleRequired = MAX_CIRCLE_AREA_FACTOR;
+ } else {
+ scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
+ }
+
+ float areaScale = hullArea / fullArea;
+ // Use sqrt of the final ratio as the images is scaled across both width and height.
+ return areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
+ }
+
+ /**
+ * @param d Should be AdaptiveIconDrawable
+ * @param size Canvas size to use
+ */
+ @TargetApi(Build.VERSION_CODES.O)
+ public static float normalizeAdaptiveIcon(Drawable d, int size, @Nullable RectF outBounds) {
+ Rect tmpBounds = new Rect(d.getBounds());
+ d.setBounds(0, 0, size, size);
+
+ Path path = ((AdaptiveIconDrawable) d).getIconMask();
+ Region region = new Region();
+ region.setPath(path, new Region(0, 0, size, size));
+
+ Rect hullBounds = region.getBounds();
+ int hullArea = GraphicsUtils.getArea(region);
+
+ if (outBounds != null) {
+ float sizeF = size;
+ outBounds.set(
+ hullBounds.left / sizeF,
+ hullBounds.top / sizeF,
+ 1 - (hullBounds.right / sizeF),
+ 1 - (hullBounds.bottom / sizeF));
+ }
+ d.setBounds(tmpBounds);
+ return getScale(hullArea, hullArea, size * size);
}
/**
@@ -192,17 +238,14 @@ public class IconNormalizer {
*/
public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds,
@Nullable Path path, @Nullable boolean[] outMaskShape) {
- if (Utilities.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) {
- if (mAdaptiveIconScale != SCALE_NOT_INITIALIZED) {
- if (outBounds != null) {
- outBounds.set(mAdaptiveIconBounds);
- }
- return mAdaptiveIconScale;
+ if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) {
+ if (mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
+ mAdaptiveIconScale = normalizeAdaptiveIcon(d, mMaxSize, mAdaptiveIconBounds);
}
- if (d instanceof FolderAdaptiveIcon) {
- // Since we just want the scale, avoid heavy drawing operations
- d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
+ if (outBounds != null) {
+ outBounds.set(mAdaptiveIconBounds);
}
+ return mAdaptiveIconScale;
}
int width = d.getIntrinsicWidth();
int height = d.getIntrinsicHeight();
@@ -285,16 +328,6 @@ public class IconNormalizer {
area += mRightBorder[y] - mLeftBorder[y] + 1;
}
- // Area of the rectangle required to fit the convex hull
- float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
- float hullByRect = area / rectArea;
-
- float scaleRequired;
- if (hullByRect < CIRCLE_AREA_BY_RECT) {
- scaleRequired = MAX_CIRCLE_AREA_FACTOR;
- } else {
- scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
- }
mBounds.left = leftX;
mBounds.right = rightX;
@@ -306,19 +339,12 @@ public class IconNormalizer {
1 - ((float) mBounds.right) / width,
1 - ((float) mBounds.bottom) / height);
}
-
- if (outMaskShape != null && outMaskShape.length > 0) {
+ if (outMaskShape != null && mEnableShapeDetection && outMaskShape.length > 0) {
outMaskShape[0] = isShape(path);
}
- float areaScale = area / (width * height);
- // Use sqrt of the final ratio as the images is scaled across both width and height.
- float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
- if (Utilities.ATLEAST_OREO && d instanceof AdaptiveIconDrawable &&
- mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
- mAdaptiveIconScale = scale;
- mAdaptiveIconBounds.set(mBounds);
- }
- return scale;
+ // Area of the rectangle required to fit the convex hull
+ float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
+ return getScale(area, rectArea, width * height);
}
/**
diff --git a/src/com/android/launcher3/graphics/ShadowGenerator.java b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
similarity index 91%
rename from src/com/android/launcher3/graphics/ShadowGenerator.java
rename to iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
index 88da853d696e4773ddaec882b213ba0fd0670b22..5df80439048512006b6800cb0a3739711b6d183e 100644
--- a/src/com/android/launcher3/graphics/ShadowGenerator.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package com.android.launcher3.graphics;
+package com.android.launcher3.icons;
+
+import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
-import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BlurMaskFilter;
@@ -27,23 +28,18 @@ import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
-import android.support.v4.graphics.ColorUtils;
-
-import com.android.launcher3.LauncherAppState;
/**
* Utility class to add shadows to bitmaps.
*/
public class ShadowGenerator {
-
- // Percent of actual icon size
- private static final float HALF_DISTANCE = 0.5f;
public static final float BLUR_FACTOR = 0.5f/48;
// Percent of actual icon size
public static final float KEY_SHADOW_DISTANCE = 1f/48;
private static final int KEY_SHADOW_ALPHA = 61;
-
+ // Percent of actual icon size
+ private static final float HALF_DISTANCE = 0.5f;
private static final int AMBIENT_SHADOW_ALPHA = 30;
private final int mIconSize;
@@ -52,8 +48,8 @@ public class ShadowGenerator {
private final Paint mDrawPaint;
private final BlurMaskFilter mDefaultBlurMaskFilter;
- public ShadowGenerator(Context context) {
- mIconSize = LauncherAppState.getIDP(context).iconBitmapSize;
+ public ShadowGenerator(int iconSize) {
+ mIconSize = iconSize;
mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL);
@@ -120,13 +116,17 @@ public class ShadowGenerator {
}
public Builder setupBlurForSize(int height) {
- shadowBlur = height * 1f / 32;
+ shadowBlur = height * 1f / 24;
keyShadowDistance = height * 1f / 16;
return this;
}
public Bitmap createPill(int width, int height) {
- radius = height / 2f;
+ return createPill(width, height, height / 2f);
+ }
+
+ public Bitmap createPill(int width, int height, float r) {
+ radius = r;
int centerX = Math.round(width / 2f + shadowBlur);
int centerY = Math.round(radius + shadowBlur + keyShadowDistance);
@@ -146,12 +146,12 @@ public class ShadowGenerator {
// Key shadow
p.setShadowLayer(shadowBlur, 0, keyShadowDistance,
- ColorUtils.setAlphaComponent(Color.BLACK, keyShadowAlpha));
+ setColorAlphaBound(Color.BLACK, keyShadowAlpha));
c.drawRoundRect(bounds, radius, radius, p);
// Ambient shadow
p.setShadowLayer(shadowBlur, 0, 0,
- ColorUtils.setAlphaComponent(Color.BLACK, ambientShadowAlpha));
+ setColorAlphaBound(Color.BLACK, ambientShadowAlpha));
c.drawRoundRect(bounds, radius, radius, p);
if (Color.alpha(color) < 255) {
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..d84633d5636d57a5fd09861b5a3fe7c2e0ded7eb
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -0,0 +1,563 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.icons.cache;
+
+import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon;
+import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
+import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.launcher3.icons.BaseIconFactory;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.icons.GraphicsUtils;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SQLiteCacheHelper;
+
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import androidx.annotation.NonNull;
+
+public abstract class BaseIconCache {
+
+ private static final String TAG = "BaseIconCache";
+ private static final boolean DEBUG = false;
+
+ private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
+
+ // Empty class name is used for storing package default entry.
+ public static final String EMPTY_CLASS_NAME = ".";
+
+ public static class CacheEntry extends BitmapInfo {
+ public CharSequence title = "";
+ public CharSequence contentDescription = "";
+ }
+
+ private final HashMap mDefaultIcons = new HashMap<>();
+
+ protected final Context mContext;
+ protected final PackageManager mPackageManager;
+
+ private final Map mCache;
+ protected final Handler mWorkerHandler;
+
+ protected int mIconDpi;
+ protected IconDB mIconDb;
+ protected String mSystemState = "";
+
+ private final String mDbFileName;
+ private final BitmapFactory.Options mDecodeOptions;
+ private final Looper mBgLooper;
+
+ public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
+ int iconDpi, int iconPixelSize, boolean inMemoryCache) {
+ mContext = context;
+ mDbFileName = dbFileName;
+ mPackageManager = context.getPackageManager();
+ mBgLooper = bgLooper;
+ mWorkerHandler = new Handler(mBgLooper);
+
+ if (inMemoryCache) {
+ mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
+ } else {
+ // Use a dummy cache
+ mCache = new AbstractMap() {
+ @Override
+ public Set> entrySet() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public CacheEntry put(ComponentKey key, CacheEntry value) {
+ return value;
+ }
+ };
+ }
+
+ if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mDecodeOptions = new BitmapFactory.Options();
+ mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE;
+ } else {
+ mDecodeOptions = null;
+ }
+
+ updateSystemState();
+ mIconDpi = iconDpi;
+ mIconDb = new IconDB(context, dbFileName, iconPixelSize);
+ }
+
+ /**
+ * Returns the persistable serial number for {@param user}. Subclass should implement proper
+ * caching strategy to avoid making binder call every time.
+ */
+ protected abstract long getSerialNumberForUser(UserHandle user);
+
+ /**
+ * Return true if the given app is an instant app and should be badged appropriately.
+ */
+ protected abstract boolean isInstantApp(ApplicationInfo info);
+
+ /**
+ * Opens and returns an icon factory. The factory is recycled by the caller.
+ */
+ protected abstract BaseIconFactory getIconFactory();
+
+ public void updateIconParams(int iconDpi, int iconPixelSize) {
+ mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
+ }
+
+ private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
+ mIconDpi = iconDpi;
+ mDefaultIcons.clear();
+ mIconDb.clear();
+ mIconDb.close();
+ mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
+ mCache.clear();
+ }
+
+ private Drawable getFullResIcon(Resources resources, int iconId) {
+ if (resources != null && iconId != 0) {
+ try {
+ return resources.getDrawableForDensity(iconId, mIconDpi);
+ } catch (Resources.NotFoundException e) { }
+ }
+ return getFullResDefaultActivityIcon(mIconDpi);
+ }
+
+ public Drawable getFullResIcon(String packageName, int iconId) {
+ try {
+ return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
+ } catch (PackageManager.NameNotFoundException e) { }
+ return getFullResDefaultActivityIcon(mIconDpi);
+ }
+
+ public Drawable getFullResIcon(ActivityInfo info) {
+ try {
+ return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
+ info.getIconResource());
+ } catch (PackageManager.NameNotFoundException e) { }
+ return getFullResDefaultActivityIcon(mIconDpi);
+ }
+
+ private BitmapInfo makeDefaultIcon(UserHandle user) {
+ try (BaseIconFactory li = getIconFactory()) {
+ return li.makeDefaultIcon(user);
+ }
+ }
+
+ /**
+ * Remove any records for the supplied ComponentName.
+ */
+ public synchronized void remove(ComponentName componentName, UserHandle user) {
+ mCache.remove(new ComponentKey(componentName, user));
+ }
+
+ /**
+ * Remove any records for the supplied package name from memory.
+ */
+ private void removeFromMemCacheLocked(String packageName, UserHandle user) {
+ HashSet forDeletion = new HashSet<>();
+ for (ComponentKey key: mCache.keySet()) {
+ if (key.componentName.getPackageName().equals(packageName)
+ && key.user.equals(user)) {
+ forDeletion.add(key);
+ }
+ }
+ for (ComponentKey condemned: forDeletion) {
+ mCache.remove(condemned);
+ }
+ }
+
+ /**
+ * Removes the entries related to the given package in memory and persistent DB.
+ */
+ public synchronized void removeIconsForPkg(String packageName, UserHandle user) {
+ removeFromMemCacheLocked(packageName, user);
+ long userSerial = getSerialNumberForUser(user);
+ mIconDb.delete(
+ IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
+ new String[]{packageName + "/%", Long.toString(userSerial)});
+ }
+
+ public IconCacheUpdateHandler getUpdateHandler() {
+ updateSystemState();
+ return new IconCacheUpdateHandler(this);
+ }
+
+ /**
+ * Refreshes the system state definition used to check the validity of the cache. It
+ * incorporates all the properties that can affect the cache like locale and system-version.
+ */
+ private void updateSystemState() {
+ final String locale =
+ mContext.getResources().getConfiguration().getLocales().toLanguageTags();
+ mSystemState = locale + "," + Build.VERSION.SDK_INT;
+ }
+
+ protected String getIconSystemState(String packageName) {
+ return mSystemState;
+ }
+
+ /**
+ * Adds an entry into the DB and the in-memory cache.
+ * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
+ * the memory. This is useful then the previous bitmap was created using
+ * old data.
+ * package private
+ */
+ protected synchronized void addIconToDBAndMemCache(T object, CachingLogic cachingLogic,
+ PackageInfo info, long userSerial, boolean replaceExisting) {
+ UserHandle user = cachingLogic.getUser(object);
+ ComponentName componentName = cachingLogic.getComponent(object);
+
+ final ComponentKey key = new ComponentKey(componentName, user);
+ CacheEntry entry = null;
+ if (!replaceExisting) {
+ entry = mCache.get(key);
+ // We can't reuse the entry if the high-res icon is not present.
+ if (entry == null || entry.icon == null || entry.isLowRes()) {
+ entry = null;
+ }
+ }
+ if (entry == null) {
+ entry = new CacheEntry();
+ cachingLogic.loadIcon(mContext, object, entry);
+ }
+ entry.title = cachingLogic.getLabel(object);
+ entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+ mCache.put(key, entry);
+
+ ContentValues values = newContentValues(entry, entry.title.toString(),
+ componentName.getPackageName());
+ addIconToDB(values, componentName, info, userSerial);
+ }
+
+ /**
+ * Updates {@param values} to contain versioning information and adds it to the DB.
+ * @param values {@link ContentValues} containing icon & title
+ */
+ private void addIconToDB(ContentValues values, ComponentName key,
+ PackageInfo info, long userSerial) {
+ values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
+ values.put(IconDB.COLUMN_USER, userSerial);
+ values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
+ values.put(IconDB.COLUMN_VERSION, info.versionCode);
+ mIconDb.insertOrReplace(values);
+ }
+
+ public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
+ if (!mDefaultIcons.containsKey(user)) {
+ mDefaultIcons.put(user, makeDefaultIcon(user));
+ }
+ return mDefaultIcons.get(user);
+ }
+
+ public boolean isDefaultIcon(Bitmap icon, UserHandle user) {
+ return getDefaultIcon(user).icon == icon;
+ }
+
+ /**
+ * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
+ * This method is not thread safe, it must be called from a synchronized method.
+ */
+ protected CacheEntry cacheLocked(
+ @NonNull ComponentName componentName, @NonNull UserHandle user,
+ @NonNull Supplier infoProvider, @NonNull CachingLogic cachingLogic,
+ boolean usePackageIcon, boolean useLowResIcon) {
+ return cacheLocked(componentName, user, infoProvider, cachingLogic, usePackageIcon,
+ useLowResIcon, true);
+ }
+
+ protected CacheEntry cacheLocked(
+ @NonNull ComponentName componentName, @NonNull UserHandle user,
+ @NonNull Supplier infoProvider, @NonNull CachingLogic cachingLogic,
+ boolean usePackageIcon, boolean useLowResIcon, boolean addToMemCache) {
+ assertWorkerThread();
+ ComponentKey cacheKey = new ComponentKey(componentName, user);
+ CacheEntry entry = mCache.get(cacheKey);
+ if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
+ entry = new CacheEntry();
+ if (addToMemCache) {
+ mCache.put(cacheKey, entry);
+ }
+
+ // Check the DB first.
+ T object = null;
+ boolean providerFetchedOnce = false;
+
+ if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
+ object = infoProvider.get();
+ providerFetchedOnce = true;
+
+ if (object != null) {
+ cachingLogic.loadIcon(mContext, object, entry);
+ } else {
+ if (usePackageIcon) {
+ CacheEntry packageEntry = getEntryForPackageLocked(
+ componentName.getPackageName(), user, false);
+ if (packageEntry != null) {
+ if (DEBUG) Log.d(TAG, "using package default icon for " +
+ componentName.toShortString());
+ packageEntry.applyTo(entry);
+ entry.title = packageEntry.title;
+ entry.contentDescription = packageEntry.contentDescription;
+ }
+ }
+ if (entry.icon == null) {
+ if (DEBUG) Log.d(TAG, "using default icon for " +
+ componentName.toShortString());
+ getDefaultIcon(user).applyTo(entry);
+ }
+ }
+ }
+
+ if (TextUtils.isEmpty(entry.title)) {
+ if (object == null && !providerFetchedOnce) {
+ object = infoProvider.get();
+ providerFetchedOnce = true;
+ }
+ if (object != null) {
+ entry.title = cachingLogic.getLabel(object);
+ entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+ }
+ }
+ }
+ return entry;
+ }
+
+ public synchronized void clear() {
+ assertWorkerThread();
+ mIconDb.clear();
+ }
+
+ /**
+ * Adds a default package entry in the cache. This entry is not persisted and will be removed
+ * when the cache is flushed.
+ */
+ public synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
+ Bitmap icon, CharSequence title) {
+ removeFromMemCacheLocked(packageName, user);
+
+ ComponentKey cacheKey = getPackageKey(packageName, user);
+ CacheEntry entry = mCache.get(cacheKey);
+
+ // For icon caching, do not go through DB. Just update the in-memory entry.
+ if (entry == null) {
+ entry = new CacheEntry();
+ }
+ if (!TextUtils.isEmpty(title)) {
+ entry.title = title;
+ }
+ if (icon != null) {
+ BaseIconFactory li = getIconFactory();
+ li.createIconBitmap(icon).applyTo(entry);
+ li.close();
+ }
+ if (!TextUtils.isEmpty(title) && entry.icon != null) {
+ mCache.put(cacheKey, entry);
+ }
+ }
+
+ private static ComponentKey getPackageKey(String packageName, UserHandle user) {
+ ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
+ return new ComponentKey(cn, user);
+ }
+
+ /**
+ * Gets an entry for the package, which can be used as a fallback entry for various components.
+ * This method is not thread safe, it must be called from a synchronized method.
+ */
+ protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
+ boolean useLowResIcon) {
+ assertWorkerThread();
+ ComponentKey cacheKey = getPackageKey(packageName, user);
+ CacheEntry entry = mCache.get(cacheKey);
+
+ if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
+ entry = new CacheEntry();
+ boolean entryUpdated = true;
+
+ // Check the DB first.
+ if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
+ try {
+ int flags = Process.myUserHandle().equals(user) ? 0 :
+ PackageManager.GET_UNINSTALLED_PACKAGES;
+ PackageInfo info = mPackageManager.getPackageInfo(packageName, flags);
+ ApplicationInfo appInfo = info.applicationInfo;
+ if (appInfo == null) {
+ throw new NameNotFoundException("ApplicationInfo is null");
+ }
+
+ BaseIconFactory li = getIconFactory();
+ // Load the full res icon for the application, but if useLowResIcon is set, then
+ // only keep the low resolution icon instead of the larger full-sized icon
+ BitmapInfo iconInfo = li.createBadgedIconBitmap(
+ appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion,
+ isInstantApp(appInfo));
+ li.close();
+
+ entry.title = appInfo.loadLabel(mPackageManager);
+ entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+ entry.icon = useLowResIcon ? LOW_RES_ICON : iconInfo.icon;
+ entry.color = iconInfo.color;
+
+ // Add the icon in the DB here, since these do not get written during
+ // package updates.
+ ContentValues values = newContentValues(
+ iconInfo, entry.title.toString(), packageName);
+ addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user));
+
+ } catch (NameNotFoundException e) {
+ if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
+ entryUpdated = false;
+ }
+ }
+
+ // Only add a filled-out entry to the cache
+ if (entryUpdated) {
+ mCache.put(cacheKey, entry);
+ }
+ }
+ return entry;
+ }
+
+ private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
+ Cursor c = null;
+ try {
+ c = mIconDb.query(
+ lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
+ IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
+ new String[]{
+ cacheKey.componentName.flattenToString(),
+ Long.toString(getSerialNumberForUser(cacheKey.user))});
+ if (c.moveToNext()) {
+ // Set the alpha to be 255, so that we never have a wrong color
+ entry.color = setColorAlphaBound(c.getInt(0), 255);
+ entry.title = c.getString(1);
+ if (entry.title == null) {
+ entry.title = "";
+ entry.contentDescription = "";
+ } else {
+ entry.contentDescription = mPackageManager.getUserBadgedLabel(
+ entry.title, cacheKey.user);
+ }
+
+ if (lowRes) {
+ entry.icon = LOW_RES_ICON;
+ } else {
+ byte[] data = c.getBlob(2);
+ try {
+ entry.icon = BitmapFactory.decodeByteArray(data, 0, data.length,
+ mDecodeOptions);
+ } catch (Exception e) { }
+ }
+ return true;
+ }
+ } catch (SQLiteException e) {
+ Log.d(TAG, "Error reading icon cache", e);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return false;
+ }
+
+ static final class IconDB extends SQLiteCacheHelper {
+ private final static int RELEASE_VERSION = 26;
+
+ public final static String TABLE_NAME = "icons";
+ public final static String COLUMN_ROWID = "rowid";
+ public final static String COLUMN_COMPONENT = "componentName";
+ public final static String COLUMN_USER = "profileId";
+ public final static String COLUMN_LAST_UPDATED = "lastUpdated";
+ public final static String COLUMN_VERSION = "version";
+ public final static String COLUMN_ICON = "icon";
+ public final static String COLUMN_ICON_COLOR = "icon_color";
+ public final static String COLUMN_LABEL = "label";
+ public final static String COLUMN_SYSTEM_STATE = "system_state";
+
+ public final static String[] COLUMNS_HIGH_RES = new String[] {
+ IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON };
+ public final static String[] COLUMNS_LOW_RES = new String[] {
+ IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL };
+
+ public IconDB(Context context, String dbFileName, int iconPixelSize) {
+ super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME);
+ }
+
+ @Override
+ protected void onCreateTable(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
+ COLUMN_COMPONENT + " TEXT NOT NULL, " +
+ COLUMN_USER + " INTEGER NOT NULL, " +
+ COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
+ COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
+ COLUMN_ICON + " BLOB, " +
+ COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, " +
+ COLUMN_LABEL + " TEXT, " +
+ COLUMN_SYSTEM_STATE + " TEXT, " +
+ "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
+ ");");
+ }
+ }
+
+ private ContentValues newContentValues(BitmapInfo bitmapInfo, String label, String packageName) {
+ ContentValues values = new ContentValues();
+ values.put(IconDB.COLUMN_ICON,
+ bitmapInfo.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon));
+ values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
+
+ values.put(IconDB.COLUMN_LABEL, label);
+ values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName));
+
+ return values;
+ }
+
+ private void assertWorkerThread() {
+ if (Looper.myLooper() != mBgLooper) {
+ throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TransformedRect.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
similarity index 60%
rename from quickstep/src/com/android/quickstep/util/TransformedRect.java
rename to iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
index 79f11e405b6b577084b41709001cad0f9aaf7871..addb51fa7813bf8748ff2482f752b3f68cd189b1 100644
--- a/quickstep/src/com/android/quickstep/util/TransformedRect.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -13,20 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.quickstep.util;
+package com.android.launcher3.icons.cache;
-import android.graphics.Rect;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.UserHandle;
-/**
- * A wrapper around {@link Rect} with additional transformation properties
- */
-public class TransformedRect {
+import com.android.launcher3.icons.BitmapInfo;
+
+public interface CachingLogic {
+
+ ComponentName getComponent(T object);
+
+ UserHandle getUser(T object);
- public final Rect rect = new Rect();
- public float scale = 1;
+ CharSequence getLabel(T object);
- public void set(TransformedRect transformedRect) {
- rect.set(transformedRect.rect);
- scale = transformedRect.scale;
- }
+ void loadIcon(Context context, T object, BitmapInfo target);
}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java
new file mode 100644
index 0000000000000000000000000000000000000000..ee52934543cf0e480b193c18f8dd44ccb61f5198
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.icons.cache;
+
+import android.os.Handler;
+
+/**
+ * A runnable that can be posted to a {@link Handler} which can be canceled.
+ */
+public abstract class HandlerRunnable implements Runnable {
+
+ private final Handler mHandler;
+ private final Runnable mEndRunnable;
+
+ private boolean mEnded = false;
+ private boolean mCanceled = false;
+
+ public HandlerRunnable(Handler handler, Runnable endRunnable) {
+ mHandler = handler;
+ mEndRunnable = endRunnable;
+ }
+
+ /**
+ * Cancels this runnable from being run, only if it has not already run.
+ */
+ public void cancel() {
+ mHandler.removeCallbacks(this);
+ // TODO: This can actually cause onEnd to be called twice if the handler is already running
+ // this runnable
+ // NOTE: This is currently run on whichever thread the caller is run on.
+ mCanceled = true;
+ onEnd();
+ }
+
+ /**
+ * @return whether this runnable was canceled.
+ */
+ protected boolean isCanceled() {
+ return mCanceled;
+ }
+
+ /**
+ * To be called by the implemention of this runnable. The end callback is done on whichever
+ * thread the caller is calling from.
+ */
+ public void onEnd() {
+ if (!mEnded) {
+ mEnded = true;
+ if (mEndRunnable != null) {
+ mEndRunnable.run();
+ }
+ }
+ }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c71bd027441f12335c9af4d95c177b231687244
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
@@ -0,0 +1,303 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.icons.cache;
+
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+
+import com.android.launcher3.icons.cache.BaseIconCache.IconDB;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * Utility class to handle updating the Icon cache
+ */
+public class IconCacheUpdateHandler {
+
+ private static final String TAG = "IconCacheUpdateHandler";
+
+ /**
+ * In this mode, all invalid icons are marked as to-be-deleted in {@link #mItemsToDelete}.
+ * This mode is used for the first run.
+ */
+ private static final boolean MODE_SET_INVALID_ITEMS = true;
+
+ /**
+ * In this mode, any valid icon is removed from {@link #mItemsToDelete}. This is used for all
+ * subsequent runs, which essentially acts as set-union of all valid items.
+ */
+ private static final boolean MODE_CLEAR_VALID_ITEMS = false;
+
+ private static final Object ICON_UPDATE_TOKEN = new Object();
+
+ private final HashMap mPkgInfoMap;
+ private final BaseIconCache mIconCache;
+
+ private final HashMap> mPackagesToIgnore = new HashMap<>();
+
+ private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray();
+ private boolean mFilterMode = MODE_SET_INVALID_ITEMS;
+
+ IconCacheUpdateHandler(BaseIconCache cache) {
+ mIconCache = cache;
+
+ mPkgInfoMap = new HashMap<>();
+
+ // Remove all active icon update tasks.
+ mIconCache.mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN);
+
+ createPackageInfoMap();
+ }
+
+ public void setPackagesToIgnore(UserHandle userHandle, Set packages) {
+ mPackagesToIgnore.put(userHandle, packages);
+ }
+
+ private void createPackageInfoMap() {
+ PackageManager pm = mIconCache.mPackageManager;
+ for (PackageInfo info :
+ pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES)) {
+ mPkgInfoMap.put(info.packageName, info);
+ }
+ }
+
+ /**
+ * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
+ * the DB and are updated.
+ * @return The set of packages for which icons have updated.
+ */
+ public void updateIcons(List apps, CachingLogic cachingLogic,
+ OnUpdateCallback onUpdateCallback) {
+ // Filter the list per user
+ HashMap> userComponentMap = new HashMap<>();
+ int count = apps.size();
+ for (int i = 0; i < count; i++) {
+ T app = apps.get(i);
+ UserHandle userHandle = cachingLogic.getUser(app);
+ HashMap componentMap = userComponentMap.get(userHandle);
+ if (componentMap == null) {
+ componentMap = new HashMap<>();
+ userComponentMap.put(userHandle, componentMap);
+ }
+ componentMap.put(cachingLogic.getComponent(app), app);
+ }
+
+ for (Entry> entry : userComponentMap.entrySet()) {
+ updateIconsPerUser(entry.getKey(), entry.getValue(), cachingLogic, onUpdateCallback);
+ }
+
+ // From now on, clear every valid item from the global valid map.
+ mFilterMode = MODE_CLEAR_VALID_ITEMS;
+ }
+
+ /**
+ * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
+ * the DB and are updated.
+ * @return The set of packages for which icons have updated.
+ */
+ @SuppressWarnings("unchecked")
+ private void updateIconsPerUser(UserHandle user, HashMap componentMap,
+ CachingLogic cachingLogic, OnUpdateCallback onUpdateCallback) {
+ Set ignorePackages = mPackagesToIgnore.get(user);
+ if (ignorePackages == null) {
+ ignorePackages = Collections.emptySet();
+ }
+ long userSerial = mIconCache.getSerialNumberForUser(user);
+
+ Stack appsToUpdate = new Stack<>();
+
+ try (Cursor c = mIconCache.mIconDb.query(
+ new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
+ IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
+ IconDB.COLUMN_SYSTEM_STATE},
+ IconDB.COLUMN_USER + " = ? ",
+ new String[]{Long.toString(userSerial)})) {
+
+ final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
+ final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
+ final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
+ final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
+ final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
+
+ while (c.moveToNext()) {
+ String cn = c.getString(indexComponent);
+ ComponentName component = ComponentName.unflattenFromString(cn);
+ PackageInfo info = mPkgInfoMap.get(component.getPackageName());
+
+ int rowId = c.getInt(rowIndex);
+ if (info == null) {
+ if (!ignorePackages.contains(component.getPackageName())) {
+
+ if (mFilterMode == MODE_SET_INVALID_ITEMS) {
+ mIconCache.remove(component, user);
+ mItemsToDelete.put(rowId, true);
+ }
+ }
+ continue;
+ }
+ if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
+ // Application is not present
+ continue;
+ }
+
+ long updateTime = c.getLong(indexLastUpdate);
+ int version = c.getInt(indexVersion);
+ T app = componentMap.remove(component);
+ if (version == info.versionCode && updateTime == info.lastUpdateTime &&
+ TextUtils.equals(c.getString(systemStateIndex),
+ mIconCache.getIconSystemState(info.packageName))) {
+
+ if (mFilterMode == MODE_CLEAR_VALID_ITEMS) {
+ mItemsToDelete.put(rowId, false);
+ }
+ continue;
+ }
+ if (app == null) {
+ if (mFilterMode == MODE_SET_INVALID_ITEMS) {
+ mIconCache.remove(component, user);
+ mItemsToDelete.put(rowId, true);
+ }
+ } else {
+ appsToUpdate.add(app);
+ }
+ }
+ } catch (SQLiteException e) {
+ Log.d(TAG, "Error reading icon cache", e);
+ // Continue updating whatever we have read so far
+ }
+
+ // Insert remaining apps.
+ if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
+ Stack appsToAdd = new Stack<>();
+ appsToAdd.addAll(componentMap.values());
+ new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic,
+ onUpdateCallback).scheduleNext();
+ }
+ }
+
+ /**
+ * Commits all updates as part of the update handler to disk. Not more calls should be made
+ * to this class after this.
+ */
+ public void finish() {
+ // Commit all deletes
+ int deleteCount = 0;
+ StringBuilder queryBuilder = new StringBuilder()
+ .append(IconDB.COLUMN_ROWID)
+ .append(" IN (");
+
+ int count = mItemsToDelete.size();
+ for (int i = 0; i < count; i++) {
+ if (mItemsToDelete.valueAt(i)) {
+ if (deleteCount > 0) {
+ queryBuilder.append(", ");
+ }
+ queryBuilder.append(mItemsToDelete.keyAt(i));
+ deleteCount++;
+ }
+ }
+ queryBuilder.append(')');
+
+ if (deleteCount > 0) {
+ mIconCache.mIconDb.delete(queryBuilder.toString(), null);
+ }
+ }
+
+
+ /**
+ * A runnable that updates invalid icons and adds missing icons in the DB for the provided
+ * LauncherActivityInfo list. Items are updated/added one at a time, so that the
+ * worker thread doesn't get blocked.
+ */
+ private class SerializedIconUpdateTask implements Runnable {
+ private final long mUserSerial;
+ private final UserHandle mUserHandle;
+ private final Stack mAppsToAdd;
+ private final Stack mAppsToUpdate;
+ private final CachingLogic mCachingLogic;
+ private final HashSet mUpdatedPackages = new HashSet<>();
+ private final OnUpdateCallback mOnUpdateCallback;
+
+ SerializedIconUpdateTask(long userSerial, UserHandle userHandle,
+ Stack appsToAdd, Stack appsToUpdate, CachingLogic cachingLogic,
+ OnUpdateCallback onUpdateCallback) {
+ mUserHandle = userHandle;
+ mUserSerial = userSerial;
+ mAppsToAdd = appsToAdd;
+ mAppsToUpdate = appsToUpdate;
+ mCachingLogic = cachingLogic;
+ mOnUpdateCallback = onUpdateCallback;
+ }
+
+ @Override
+ public void run() {
+ if (!mAppsToUpdate.isEmpty()) {
+ T app = mAppsToUpdate.pop();
+ String pkg = mCachingLogic.getComponent(app).getPackageName();
+ PackageInfo info = mPkgInfoMap.get(pkg);
+ mIconCache.addIconToDBAndMemCache(
+ app, mCachingLogic, info, mUserSerial, true /*replace existing*/);
+ mUpdatedPackages.add(pkg);
+
+ if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
+ // No more app to update. Notify callback.
+ mOnUpdateCallback.onPackageIconsUpdated(mUpdatedPackages, mUserHandle);
+ }
+
+ // Let it run one more time.
+ scheduleNext();
+ } else if (!mAppsToAdd.isEmpty()) {
+ T app = mAppsToAdd.pop();
+ PackageInfo info = mPkgInfoMap.get(mCachingLogic.getComponent(app).getPackageName());
+ // We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every
+ // app should have package info, this is not guaranteed by the api
+ if (info != null) {
+ mIconCache.addIconToDBAndMemCache(app, mCachingLogic, info,
+ mUserSerial, false /*replace existing*/);
+ }
+
+ if (!mAppsToAdd.isEmpty()) {
+ scheduleNext();
+ }
+ }
+ }
+
+ public void scheduleNext() {
+ mIconCache.mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN,
+ SystemClock.uptimeMillis() + 1);
+ }
+ }
+
+ public interface OnUpdateCallback {
+
+ void onPackageIconsUpdated(HashSet updatedPackages, UserHandle user);
+ }
+}
diff --git a/src/com/android/launcher3/util/ComponentKey.java b/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java
similarity index 93%
rename from src/com/android/launcher3/util/ComponentKey.java
rename to iconloaderlib/src/com/android/launcher3/util/ComponentKey.java
index d478ffa5416423983efe40361fa69bbf0d82b249..34bed942702cccedfdfa52487fbdd3bfc2775408 100644
--- a/src/com/android/launcher3/util/ComponentKey.java
+++ b/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java
@@ -29,8 +29,9 @@ public class ComponentKey {
private final int mHashCode;
public ComponentKey(ComponentName componentName, UserHandle user) {
- Preconditions.assertNotNull(componentName);
- Preconditions.assertNotNull(user);
+ if (componentName == null || user == null) {
+ throw new NullPointerException();
+ }
this.componentName = componentName;
this.user = user;
mHashCode = Arrays.hashCode(new Object[] {componentName, user});
diff --git a/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java b/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
similarity index 94%
rename from src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
rename to iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
index 05a7d2743453fe621c06e875a86b428a1d8b7dce..fe864a2847641fcc105fb0202b89da347614bafa 100644
--- a/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
+++ b/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
@@ -18,8 +18,6 @@ package com.android.launcher3.util;
import static android.database.sqlite.SQLiteDatabase.NO_LOCALIZED_COLLATORS;
-import static com.android.launcher3.Utilities.ATLEAST_P;
-
import android.content.Context;
import android.content.ContextWrapper;
import android.database.DatabaseErrorHandler;
@@ -27,6 +25,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteDatabase.OpenParams;
import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Build;
/**
* Extension of {@link SQLiteOpenHelper} which avoids creating default locale table by
@@ -34,6 +33,9 @@ import android.database.sqlite.SQLiteOpenHelper;
*/
public abstract class NoLocaleSQLiteHelper extends SQLiteOpenHelper {
+ private static final boolean ATLEAST_P =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+
public NoLocaleSQLiteHelper(Context context, String name, int version) {
super(ATLEAST_P ? context : new NoLocalContext(context), name, null, version);
if (ATLEAST_P) {
diff --git a/src/com/android/launcher3/util/SQLiteCacheHelper.java b/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java
similarity index 93%
rename from src/com/android/launcher3/util/SQLiteCacheHelper.java
rename to iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java
index 44c1762c34d46cdbcaabe16415b6d6b5b19cf945..49de4bd1bfbf671ca462d818a44553acea2e42b5 100644
--- a/src/com/android/launcher3/util/SQLiteCacheHelper.java
+++ b/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java
@@ -9,9 +9,6 @@ import android.database.sqlite.SQLiteFullException;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-
/**
* An extension of {@link SQLiteOpenHelper} with utility methods for a single table cache DB.
* Any exception during write operations are ignored, and any version change causes a DB reset.
@@ -19,8 +16,7 @@ import com.android.launcher3.config.FeatureFlags;
public abstract class SQLiteCacheHelper {
private static final String TAG = "SQLiteCacheHelper";
- private static final boolean NO_ICON_CACHE = FeatureFlags.IS_DOGFOOD_BUILD &&
- Utilities.isPropertyEnabled(LogConfig.MEMORY_ONLY_ICON_CACHE);
+ private static final boolean IN_MEMORY_CACHE = false;
private final String mTableName;
private final MySQLiteOpenHelper mOpenHelper;
@@ -28,7 +24,7 @@ public abstract class SQLiteCacheHelper {
private boolean mIgnoreWrites;
public SQLiteCacheHelper(Context context, String name, int version, String tableName) {
- if (NO_ICON_CACHE) {
+ if (IN_MEMORY_CACHE) {
name = null;
}
mTableName = tableName;
@@ -87,6 +83,10 @@ public abstract class SQLiteCacheHelper {
mOpenHelper.clearDB(mOpenHelper.getWritableDatabase());
}
+ public void close() {
+ mOpenHelper.close();
+ }
+
protected abstract void onCreateTable(SQLiteDatabase db);
/**
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..48f11fde3bdd3ca25bde9c82903e2b4c44c0d579
--- /dev/null
+++ b/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+package com.android.launcher3.icons;
+
+import android.content.Context;
+
+/**
+ * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
+ * that are threadsafe.
+ */
+public class IconFactory extends BaseIconFactory {
+
+ private static final Object sPoolSync = new Object();
+ private static IconFactory sPool;
+ private static int sPoolId = 0;
+
+ /**
+ * Return a new Message instance from the global pool. Allows us to
+ * avoid allocating new objects in many cases.
+ */
+ public static IconFactory obtain(Context context) {
+ int poolId;
+ synchronized (sPoolSync) {
+ if (sPool != null) {
+ IconFactory m = sPool;
+ sPool = m.next;
+ m.next = null;
+ return m;
+ }
+ poolId = sPoolId;
+ }
+
+ return new IconFactory(context,
+ context.getResources().getConfiguration().densityDpi,
+ context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size),
+ poolId);
+ }
+
+ public static void clearPool() {
+ synchronized (sPoolSync) {
+ sPool = null;
+ sPoolId++;
+ }
+ }
+
+ private final int mPoolId;
+
+ private IconFactory next;
+
+ private IconFactory(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) {
+ super(context, fillResIconDpi, iconBitmapSize);
+ mPoolId = poolId;
+ }
+
+ /**
+ * Recycles a LauncherIcons that may be in-use.
+ */
+ public void recycle() {
+ synchronized (sPoolSync) {
+ if (sPoolId != mPoolId) {
+ return;
+ }
+ // Clear any temporary state variables
+ clear();
+
+ next = sPool;
+ sPool = this;
+ }
+ }
+
+ @Override
+ public void close() {
+ recycle();
+ }
+}
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..1337975f196af02f74e87df4cf5b42ef99b6816e
--- /dev/null
+++ b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.icons;
+
+import static android.content.Intent.ACTION_MANAGED_PROFILE_ADDED;
+import static android.content.Intent.ACTION_MANAGED_PROFILE_REMOVED;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.SparseLongArray;
+
+import com.android.launcher3.icons.cache.BaseIconCache;
+
+/**
+ * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
+ * that are threadsafe.
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class SimpleIconCache extends BaseIconCache {
+
+ private static SimpleIconCache sIconCache = null;
+ private static final Object CACHE_LOCK = new Object();
+
+ private final SparseLongArray mUserSerialMap = new SparseLongArray(2);
+ private final UserManager mUserManager;
+
+ public SimpleIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi,
+ int iconPixelSize, boolean inMemoryCache) {
+ super(context, dbFileName, bgLooper, iconDpi, iconPixelSize, inMemoryCache);
+ mUserManager = context.getSystemService(UserManager.class);
+
+ // Listen for user cache changes.
+ IntentFilter filter = new IntentFilter(ACTION_MANAGED_PROFILE_ADDED);
+ filter.addAction(ACTION_MANAGED_PROFILE_REMOVED);
+ context.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ resetUserCache();
+ }
+ }, filter, null, new Handler(bgLooper), 0);
+ }
+
+ @Override
+ protected long getSerialNumberForUser(UserHandle user) {
+ synchronized (mUserSerialMap) {
+ int index = mUserSerialMap.indexOfKey(user.getIdentifier());
+ if (index >= 0) {
+ return mUserSerialMap.valueAt(index);
+ }
+ long serial = mUserManager.getSerialNumberForUser(user);
+ mUserSerialMap.put(user.getIdentifier(), serial);
+ return serial;
+ }
+ }
+
+ private void resetUserCache() {
+ synchronized (mUserSerialMap) {
+ mUserSerialMap.clear();
+ }
+ }
+
+ @Override
+ protected boolean isInstantApp(ApplicationInfo info) {
+ return info.isInstantApp();
+ }
+
+ @Override
+ protected BaseIconFactory getIconFactory() {
+ return IconFactory.obtain(mContext);
+ }
+
+ public static SimpleIconCache getIconCache(Context context) {
+ synchronized (CACHE_LOCK) {
+ if (sIconCache != null) {
+ return sIconCache;
+ }
+ boolean inMemoryCache =
+ context.getResources().getBoolean(R.bool.simple_cache_enable_im_memory);
+ String dbFileName = context.getString(R.string.cache_db_name);
+
+ HandlerThread bgThread = new HandlerThread("simple-icon-cache");
+ bgThread.start();
+
+ sIconCache = new SimpleIconCache(context.getApplicationContext(), dbFileName,
+ bgThread.getLooper(), context.getResources().getConfiguration().densityDpi,
+ context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size),
+ inMemoryCache);
+ return sIconCache;
+ }
+ }
+}
diff --git a/proguard.flags b/proguard.flags
index e4011165e144bc3c2799b5bf90e22d58f4460ad1..272ab7a7f1f6a9f7a3e34a59d186eed2c619071d 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -2,80 +2,6 @@
*;
}
--keep class com.android.launcher3.allapps.AllAppsBackgroundDrawable {
- public void setAlpha(int);
- public int getAlpha();
-}
-
--keep class com.android.launcher3.BaseRecyclerViewFastScrollBar {
- public void setThumbWidth(int);
- public int getThumbWidth();
- public void setTrackWidth(int);
- public int getTrackWidth();
-}
-
--keep class com.android.launcher3.BaseRecyclerViewFastScrollPopup {
- public void setAlpha(float);
- public float getAlpha();
-}
-
--keep class com.android.launcher3.ButtonDropTarget {
- public int getTextColor();
-}
-
--keep class com.android.launcher3.CellLayout {
- public float getBackgroundAlpha();
- public void setBackgroundAlpha(float);
-}
-
--keep class com.android.launcher3.CellLayout$LayoutParams {
- public void setWidth(int);
- public int getWidth();
- public void setHeight(int);
- public int getHeight();
- public void setX(int);
- public int getX();
- public void setY(int);
- public int getY();
-}
-
--keep class com.android.launcher3.views.BaseDragLayer$LayoutParams {
- public void setWidth(int);
- public int getWidth();
- public void setHeight(int);
- public int getHeight();
- public void setX(int);
- public int getX();
- public void setY(int);
- public int getY();
-}
-
--keep class com.android.launcher3.FastBitmapDrawable {
- public void setDesaturation(float);
- public float getDesaturation();
- public void setBrightness(float);
- public float getBrightness();
-}
-
--keep class com.android.launcher3.MemoryDumpActivity {
- *;
-}
-
--keep class com.android.launcher3.PreloadIconDrawable {
- public float getAnimationProgress();
- public void setAnimationProgress(float);
-}
-
--keep class com.android.launcher3.pageindicators.CaretDrawable {
- public float getCaretProgress();
- public void setCaretProgress(float);
-}
-
--keep class com.android.launcher3.Workspace {
- public float getBackgroundAlpha();
- public void setBackgroundAlpha(float);
-}
-
# Proguard will strip new callbacks in LauncherApps.Callback from
# WrappedCallback if compiled against an older SDK. Don't let this happen.
-keep class com.android.launcher3.compat.** {
@@ -95,32 +21,21 @@
# next row when focus is on the last item of last row when using a RecyclerView
# Keep optimized and shrunk proguard to prevent issues like this when using
# support jar.
--keep class android.support.v7.widget.RecyclerView { *; }
-
-# LauncherAppTransitionManager
--keep class com.android.launcher3.LauncherAppTransitionManagerImpl {
- public (...);
-}
+-keep class androidx.recyclerview.widget.RecyclerView { *; }
-# InstantAppResolver
--keep class com.android.quickstep.InstantAppResolverImpl {
+# Preference fragments
+-keep class ** extends android.app.Fragment {
public (...);
}
-# MainProcessInitializer
--keep class com.android.quickstep.QuickstepProcessInitializer {
- public (...);
-}
-
-# UserEventDispatcherExtension
--keep class com.android.quickstep.logging.UserEventDispatcherExtension {
+## Prevent obfuscating various overridable objects
+-keep class ** implements com.android.launcher3.util.ResourceBasedOverride {
public (...);
}
-keep interface com.android.launcher3.userevent.nano.LauncherLogProto.** {
*;
}
-
-keep interface com.android.launcher3.model.nano.LauncherDumpProto.** {
*;
}
@@ -135,3 +50,4 @@
-dontwarn android.app.**
-dontwarn android.view.**
-dontwarn android.os.**
+-dontwarn android.graphics.**
\ No newline at end of file
diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto
index 06e6a923d4b1146c76c407ce21b311885b3dc3ef..49fd43617eee159d48b04275486905a387dcdfc6 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -56,6 +56,7 @@ message Target {
optional int32 predictedRank = 15;
optional TargetExtension extension = 16;
optional TipType tip_type = 17;
+ optional int32 search_query_length = 18;
}
// Used to define what type of item a Target would represent.
@@ -71,6 +72,7 @@ enum ItemType {
NOTIFICATION = 8;
TASK = 9; // Each page of Recents UI (QuickStep)
WEB_APP = 10;
+ TASK_ICON = 11;
}
// Used to define what type of container a Target would represent.
@@ -106,12 +108,16 @@ enum ControlType {
RESIZE_HANDLE = 8;
VERTICAL_SCROLL = 9;
HOME_INTENT = 10; // Deprecated, use enum Command instead
- BACK_BUTTON = 11; // Deprecated, use enum Command instead
+ BACK_BUTTON = 11;
QUICK_SCRUB_BUTTON = 12;
CLEAR_ALL_BUTTON = 13;
CANCEL_TARGET = 14;
TASK_PREVIEW = 15;
SPLIT_SCREEN_TARGET = 16;
+ REMOTE_ACTION_SHORTCUT = 17;
+ APP_USAGE_SETTINGS = 18;
+ BACK_GESTURE = 19;
+ UNDO = 20;
}
enum TipType {
@@ -120,6 +126,7 @@ enum TipType {
SWIPE_UP_TEXT = 2;
QUICK_SCRUB_TEXT = 3;
PREDICTION_TEXT = 4;
+ DWB_TOAST = 5;
}
// Used to define the action component of the LauncherEvent.
@@ -139,6 +146,7 @@ message Action {
SWIPE = 3;
FLING = 4;
PINCH = 5;
+ SWIPE_NOOP = 6;
}
enum Direction {
@@ -147,6 +155,8 @@ message Action {
DOWN = 2;
LEFT = 3;
RIGHT = 4;
+ UPRIGHT = 5;
+ UPLEFT = 6;
}
enum Command {
HOME_INTENT = 0;
@@ -184,8 +194,8 @@ message LauncherEvent {
optional int64 elapsed_container_millis = 5;
optional int64 elapsed_session_millis = 6;
- optional bool is_in_multi_window_mode = 7;
- optional bool is_in_landscape_mode = 8;
+ optional bool is_in_multi_window_mode = 7 [deprecated = true];
+ optional bool is_in_landscape_mode = 8 [deprecated = true];
optional LauncherEventExtension extension = 9;
}
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index cb7485587de005db997a3545aeaf5b89795057b8..332e0fa3604d5b8d7de8546cfad4bfedd29c2966 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -22,8 +22,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.android.launcher3" >
-
+
+ android:permission="android.permission.STATUS_BAR_SERVICE"
+ android:directBootAware="true" >
@@ -59,10 +60,10 @@
android:resumeWhilePausing="true"
android:taskAffinity="" />
-
+
-
+
+
+
+
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
deleted file mode 100644
index 27de1e9075d3b2e5504f8e714234365355592f3f..0000000000000000000000000000000000000000
Binary files a/quickstep/libs/sysui_shared.jar and /dev/null differ
diff --git a/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml b/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml
new file mode 100644
index 0000000000000000000000000000000000000000..52cc6fcb6270f122f31af260a03f2a2e3161f32a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/quickstep/recents_ui_overrides/res/drawable/chip_hint_background_light.xml b/quickstep/recents_ui_overrides/res/drawable/chip_hint_background_light.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7b4da839819f25116ed0dd8010c8e93711a64247
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/chip_hint_background_light.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b0f2b4bf8cd00921cb43414c52e69488178e852a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/quickstep/res/layout/fallback_recents_activity.xml b/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
similarity index 100%
rename from quickstep/res/layout/fallback_recents_activity.xml
rename to quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
diff --git a/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml b/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b21c34b099bfc5967d4eae994e0acd36648c75f7
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
similarity index 100%
rename from quickstep/res/layout/overview_panel.xml
rename to quickstep/recents_ui_overrides/res/layout/overview_panel.xml
diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7426e3039688b53926ecdf417d8445289852803a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/values/colors.xml
@@ -0,0 +1,9 @@
+
+
+ #fff
+
+ #61000000
+ #61FFFFFF
+ #3c000000
+ #3cffffff
+
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
new file mode 100644
index 0000000000000000000000000000000000000000..863a8ba528ffb8ef780420f899b761198ff34259
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -0,0 +1,31 @@
+
+
+ 1dp
+ 20dp
+ 20dp
+ 10dp
+ 12dp
+ 20dp
+ 2dp
+ 16dp
+ 26dp
+ 4dp
+ 10dp
+ 14sp
+
+ 17dp
+ 16dp
+ 8dp
+ 14sp
+ 8dp
+
+ 2dp
+
+
+ 80dp
+
+
+ 18dp
+ 10dp
+ -60dp
+
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1ddd3f594f4b95da21829411afb0d057dd34c87d
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/values/override.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ com.android.launcher3.LauncherAppTransitionManagerImpl
+
+ com.android.quickstep.InstantAppResolverImpl
+
+ com.android.launcher3.appprediction.PredictionAppTracker
+
+ com.android.quickstep.QuickstepProcessInitializer
+
+
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..371161ebdc562760e0fc1804607940e4d8b1e8a7
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -0,0 +1,165 @@
+/*
+ * 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.
+ */
+
+package com.android.launcher3;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
+import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
+import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;
+
+import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS;
+import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.SpringObjectAnimator;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+/**
+ * A {@link QuickstepAppTransitionManagerImpl} that also implements recents transitions from
+ * {@link RecentsView}.
+ */
+public final class LauncherAppTransitionManagerImpl extends QuickstepAppTransitionManagerImpl {
+
+ public static final int INDEX_SHELF_ANIM = 0;
+ public static final int INDEX_RECENTS_FADE_ANIM = 1;
+ public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = 2;
+
+ public LauncherAppTransitionManagerImpl(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected boolean isLaunchingFromRecents(@NonNull View v,
+ @Nullable RemoteAnimationTargetCompat[] targets) {
+ return mLauncher.getStateManager().getState().overviewUi
+ && findTaskViewToLaunch(mLauncher, v, targets) != null;
+ }
+
+ @Override
+ protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
+ @NonNull RemoteAnimationTargetCompat[] targets, boolean launcherClosing) {
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ boolean skipLauncherChanges = !launcherClosing;
+
+ TaskView taskView = findTaskViewToLaunch(mLauncher, v, targets);
+
+ ClipAnimationHelper helper = new ClipAnimationHelper(mLauncher);
+ anim.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets, helper)
+ .setDuration(RECENTS_LAUNCH_DURATION));
+
+ Animator childStateAnimation = null;
+ // Found a visible recents task that matches the opening app, lets launch the app from there
+ Animator launcherAnim;
+ final AnimatorListenerAdapter windowAnimEndListener;
+ if (launcherClosing) {
+ launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView, helper);
+ launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+ launcherAnim.setDuration(RECENTS_LAUNCH_DURATION);
+
+ // Make sure recents gets fixed up by resetting task alphas and scales, etc.
+ windowAnimEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mLauncher.getStateManager().moveToRestState();
+ mLauncher.getStateManager().reapplyState();
+ }
+ };
+ } else {
+ AnimatorPlaybackController controller =
+ mLauncher.getStateManager().createAnimationToNewWorkspace(NORMAL,
+ RECENTS_LAUNCH_DURATION);
+ controller.dispatchOnStart();
+ childStateAnimation = controller.getTarget();
+ launcherAnim = controller.getAnimationPlayer().setDuration(RECENTS_LAUNCH_DURATION);
+ windowAnimEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mLauncher.getStateManager().goToState(NORMAL, false);
+ }
+ };
+ }
+ anim.play(launcherAnim);
+
+ // Set the current animation first, before adding windowAnimEndListener. Setting current
+ // animation adds some listeners which need to be called before windowAnimEndListener
+ // (the ordering of listeners matter in this case).
+ mLauncher.getStateManager().setCurrentAnimation(anim, childStateAnimation);
+ anim.addListener(windowAnimEndListener);
+ }
+
+ @Override
+ protected Runnable composeViewContentAnimator(@NonNull AnimatorSet anim, float[] alphas,
+ float[] trans) {
+ RecentsView overview = mLauncher.getOverviewPanel();
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(overview,
+ RecentsView.CONTENT_ALPHA, alphas);
+ alpha.setDuration(CONTENT_ALPHA_DURATION);
+ alpha.setInterpolator(LINEAR);
+ anim.play(alpha);
+ overview.setFreezeViewVisibility(true);
+
+ ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans);
+ transY.setInterpolator(AGGRESSIVE_EASE);
+ transY.setDuration(CONTENT_TRANSLATION_DURATION);
+ anim.play(transY);
+
+ return () -> {
+ overview.setFreezeViewVisibility(false);
+ mLauncher.getStateManager().reapplyState();
+ };
+ }
+
+ @Override
+ public int getStateElementAnimationsCount() {
+ return 3;
+ }
+
+ @Override
+ public Animator createStateElementAnimation(int index, float... values) {
+ switch (index) {
+ case INDEX_SHELF_ANIM:
+ return mLauncher.getAllAppsController().createSpringAnimation(values);
+ case INDEX_RECENTS_FADE_ANIM:
+ return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
+ RecentsView.CONTENT_ALPHA, values);
+ case INDEX_RECENTS_TRANSLATE_X_ANIM:
+ return new SpringObjectAnimator<>(mLauncher.getOverviewPanel(),
+ VIEW_TRANSLATE_X, MIN_VISIBLE_CHANGE_PIXELS, 0.8f, 250, values);
+ default:
+ return super.createStateElementAnimation(index, values);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
new file mode 100644
index 0000000000000000000000000000000000000000..c5c4add6b2d1def4700ca08088a97ac1a8be820a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3;
+
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+
+import java.util.function.BiPredicate;
+
+public class LauncherInitListenerEx extends LauncherInitListener {
+
+ public LauncherInitListenerEx(BiPredicate onInitListener) {
+ super(onInitListener);
+ }
+
+ @Override
+ protected boolean init(Launcher launcher, boolean alreadyOnHome) {
+ PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW);
+ return super.init(launcher, alreadyOnHome);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
new file mode 100644
index 0000000000000000000000000000000000000000..d3042cf82eab6a1c9b29ccd812898b272c31f7af
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
@@ -0,0 +1,207 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.appprediction;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.quickstep.logging.UserEventDispatcherExtension.ALL_APPS_PREDICTION_TIPS;
+
+import android.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.Utilities;
+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()
+ || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+ return false;
+ }
+
+ AllAppsTipView allAppsTipView = new AllAppsTipView(launcher.getAppsView().getContext(),
+ null);
+ launcher.getDragLayer().addView(allAppsTipView);
+
+ DragLayer.LayoutParams params = (DragLayer.LayoutParams) allAppsTipView.getLayoutParams();
+ params.gravity = Gravity.CENTER_HORIZONTAL;
+
+ int top = floatingHeaderView.findFixedRowByType(PredictionRowView.class).getTop();
+ allAppsTipView.setY(top - launcher.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_tip_bottom_margin));
+
+ allAppsTipView.setAlpha(0);
+ allAppsTipView.animate()
+ .alpha(1f)
+ .withLayer()
+ .setStartDelay(SHOW_DELAY_MS)
+ .setDuration(SHOW_DURATION_MS)
+ .setInterpolator(Interpolators.DEACCEL)
+ .start();
+
+ launcher.getUserEventDispatcher().logActionTip(
+ LauncherEventUtil.VISIBLE, ALL_APPS_PREDICTION_TIPS);
+ return true;
+ }
+
+ private static boolean hasSeenAllAppsTip(Launcher launcher) {
+ return launcher.getSharedPrefs().getBoolean(ALL_APPS_TIP_SEEN, false);
+ }
+
+ public static void scheduleShowIfNeeded(Launcher launcher) {
+ if (!hasSeenAllAppsTip(launcher)) {
+ launcher.getStateManager().addStateListener(
+ new LauncherStateManager.StateListener() {
+ @Override
+ public void onStateTransitionStart(LauncherState toState) {
+ }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (finalState == ALL_APPS) {
+ if (showAllAppsTipIfNecessary(launcher)) {
+ launcher.getStateManager().removeStateListener(this);
+ }
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
new file mode 100644
index 0000000000000000000000000000000000000000..311db219353609522897a3abd89c6b765ba3f3c5
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -0,0 +1,308 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.appprediction;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.FloatingHeaderRow;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.util.Themes;
+
+import androidx.annotation.ColorInt;
+import androidx.core.content.ContextCompat;
+
+/**
+ * A view which shows a horizontal divider
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class AppsDividerView extends View implements LauncherStateManager.StateListener,
+ FloatingHeaderRow {
+
+ private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
+ private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20;
+
+ public enum DividerType {
+ NONE,
+ LINE,
+ ALL_APPS_LABEL
+ }
+
+ private final Launcher mLauncher;
+ private final TextPaint mPaint = new TextPaint();
+ private DividerType mDividerType = DividerType.NONE;
+
+ private final @ColorInt int mStrokeColor;
+ private final @ColorInt int mAllAppsLabelTextColor;
+
+ private Layout mAllAppsLabelLayout;
+ private boolean mShowAllAppsLabel;
+
+ private FloatingHeaderView mParent;
+ private boolean mTabsHidden;
+ private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS;
+
+ private boolean mIsScrolledOut = false;
+
+ public AppsDividerView(Context context) {
+ this(context, null);
+ }
+
+ public AppsDividerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(context);
+
+ boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark);
+ mPaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
+
+ mStrokeColor = ContextCompat.getColor(context, isMainColorDark
+ ? R.color.all_apps_prediction_row_separator_dark
+ : R.color.all_apps_prediction_row_separator);
+
+ mAllAppsLabelTextColor = ContextCompat.getColor(context, isMainColorDark
+ ? R.color.all_apps_label_text_dark
+ : R.color.all_apps_label_text);
+ }
+
+ public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
+ mParent = parent;
+ mTabsHidden = tabsHidden;
+ mRows = rows;
+ updateDividerType();
+ }
+
+ @Override
+ public int getExpectedHeight() {
+ return getPaddingTop() + getPaddingBottom();
+ }
+
+ @Override
+ public boolean shouldDraw() {
+ return mDividerType != DividerType.NONE;
+ }
+
+ @Override
+ public boolean hasVisibleContent() {
+ return false;
+ }
+
+ private void updateDividerType() {
+ final DividerType dividerType;
+ if (!mTabsHidden) {
+ dividerType = DividerType.NONE;
+ } else {
+ // Check how many sections above me.
+ int sectionCount = 0;
+ for (FloatingHeaderRow row : mRows) {
+ if (row == this) {
+ break;
+ } else if (row.shouldDraw()) {
+ sectionCount ++;
+ }
+ }
+
+ if (mShowAllAppsLabel && sectionCount > 0) {
+ dividerType = DividerType.ALL_APPS_LABEL;
+ } else if (sectionCount == 1) {
+ dividerType = DividerType.LINE;
+ } else {
+ dividerType = DividerType.NONE;
+ }
+ }
+
+ if (mDividerType != dividerType) {
+ mDividerType = dividerType;
+ int topPadding;
+ int bottomPadding;
+ switch (dividerType) {
+ case LINE:
+ topPadding = 0;
+ bottomPadding = getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
+ mPaint.setColor(mStrokeColor);
+ break;
+ case ALL_APPS_LABEL:
+ topPadding = getAllAppsLabelLayout().getHeight() + getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_label_top_padding);
+ bottomPadding = getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding);
+ mPaint.setColor(mAllAppsLabelTextColor);
+ break;
+ case NONE:
+ default:
+ topPadding = bottomPadding = 0;
+ break;
+ }
+ setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding);
+ updateViewVisibility();
+ invalidate();
+ requestLayout();
+ if (mParent != null) {
+ mParent.onHeightUpdated();
+ }
+ }
+ }
+
+ private void updateViewVisibility() {
+ setVisibility(mDividerType == DividerType.NONE
+ ? GONE
+ : (mIsScrolledOut ? INVISIBLE : VISIBLE));
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mDividerType == DividerType.LINE) {
+ int side = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
+ int y = getHeight() - (getPaddingBottom() / 2);
+ int x1 = getPaddingLeft() + side;
+ int x2 = getWidth() - getPaddingRight() - side;
+ canvas.drawLine(x1, y, x2, y, mPaint);
+ } else if (mDividerType == DividerType.ALL_APPS_LABEL) {
+ Layout textLayout = getAllAppsLabelLayout();
+ int x = getWidth() / 2 - textLayout.getWidth() / 2;
+ int y = getHeight() - getPaddingBottom() - textLayout.getHeight();
+ canvas.translate(x, y);
+ textLayout.draw(canvas);
+ canvas.translate(-x, -y);
+ }
+ }
+
+ private Layout getAllAppsLabelLayout() {
+ if (mAllAppsLabelLayout == null) {
+ mPaint.setAntiAlias(true);
+ mPaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
+ mPaint.setTextSize(
+ getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size));
+
+ CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label);
+ mAllAppsLabelLayout = StaticLayout.Builder.obtain(
+ allAppsLabelText, 0, allAppsLabelText.length(), mPaint,
+ Math.round(mPaint.measureText(allAppsLabelText.toString())))
+ .setAlignment(Layout.Alignment.ALIGN_CENTER)
+ .setMaxLines(1)
+ .setIncludePad(true)
+ .build();
+ }
+ return mAllAppsLabelLayout;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
+ getPaddingBottom() + getPaddingTop());
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (shouldShowAllAppsLabel()) {
+ mShowAllAppsLabel = true;
+ mLauncher.getStateManager().addStateListener(this);
+ updateDividerType();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mLauncher.getStateManager().removeStateListener(this);
+ }
+
+ @Override
+ public void onStateTransitionStart(LauncherState toState) { }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (finalState == ALL_APPS) {
+ setAllAppsVisitedCount(getAllAppsVisitedCount() + 1);
+ } else {
+ if (mShowAllAppsLabel != shouldShowAllAppsLabel()) {
+ mShowAllAppsLabel = !mShowAllAppsLabel;
+ updateDividerType();
+ }
+
+ if (!mShowAllAppsLabel) {
+ mLauncher.getStateManager().removeStateListener(this);
+ }
+ }
+ }
+
+ private void setAllAppsVisitedCount(int count) {
+ mLauncher.getSharedPrefs().edit().putInt(ALL_APPS_VISITED_COUNT, count).apply();
+ }
+
+ private int getAllAppsVisitedCount() {
+ return mLauncher.getSharedPrefs().getInt(ALL_APPS_VISITED_COUNT, 0);
+ }
+
+ private boolean shouldShowAllAppsLabel() {
+ return getAllAppsVisitedCount() < SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT;
+ }
+
+ @Override
+ public void setInsets(Rect insets, DeviceProfile grid) {
+ int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
+ + grid.cellLayoutPaddingLeftRightPx;
+ setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
+ }
+
+ @Override
+ public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
+ PropertySetter setter, Interpolator fadeInterpolator) {
+ // Don't use setViewAlpha as we want to control the visibility ourselves.
+ setter.setFloat(this, ALPHA, hasContent ? 1 : 0, fadeInterpolator);
+ }
+
+ @Override
+ public void setVerticalScroll(int scroll, boolean isScrolledOut) {
+ setTranslationY(scroll);
+ mIsScrolledOut = isScrolledOut;
+ updateViewVisibility();
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return AppsDividerView.class;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..b9f4147f850d0b39682bcb1ac63033ecd8114f38
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.appprediction;
+
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.content.Context;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ComponentKey;
+
+public class ComponentKeyMapper {
+
+ protected final ComponentKey componentKey;
+ private final Context mContext;
+ private final DynamicItemCache mCache;
+
+ public ComponentKeyMapper(Context context, ComponentKey key, DynamicItemCache cache) {
+ mContext = context;
+ componentKey = key;
+ mCache = cache;
+ }
+
+ public String getPackage() {
+ return componentKey.componentName.getPackageName();
+ }
+
+ public String getComponentClass() {
+ return componentKey.componentName.getClassName();
+ }
+
+ public ComponentKey getComponentKey() {
+ return componentKey;
+ }
+
+ @Override
+ public String toString() {
+ return componentKey.toString();
+ }
+
+ public ItemInfoWithIcon getApp(AllAppsStore store) {
+ AppInfo item = store.getApp(componentKey);
+ if (item != null) {
+ return item;
+ } else if (getComponentClass().equals(COMPONENT_CLASS_MARKER)) {
+ return mCache.getInstantApp(componentKey.componentName.getPackageName());
+ } else if (componentKey instanceof ShortcutKey) {
+ return mCache.getShortcutInfo((ShortcutKey) componentKey);
+ }
+ return null;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ecc39cf6ac3776a01f69450565c2716b3c3ac82
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.appprediction;
+
+import static android.content.pm.PackageManager.MATCH_INSTANT;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.InstantAppResolver;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+/**
+ * Utility class which loads and caches predicted items like instant apps and shortcuts, before
+ * they can be displayed on the UI
+ */
+public class DynamicItemCache {
+
+ private static final String TAG = "DynamicItemCache";
+ private static final boolean DEBUG = false;
+ private static final String DEFAULT_URL = "default-url";
+
+ private static final int BG_MSG_LOAD_SHORTCUTS = 1;
+ private static final int BG_MSG_LOAD_INSTANT_APPS = 2;
+
+ private static final int UI_MSG_UPDATE_SHORTCUTS = 1;
+ private static final int UI_MSG_UPDATE_INSTANT_APPS = 2;
+
+ private final Context mContext;
+ private final Handler mWorker;
+ private final Handler mUiHandler;
+ private final InstantAppResolver mInstantAppResolver;
+ private final Runnable mOnUpdateCallback;
+
+ private final Map mShortcuts;
+ private final Map mInstantApps;
+
+ public DynamicItemCache(Context context, Runnable onUpdateCallback) {
+ mContext = context;
+ mWorker = new Handler(LauncherModel.getWorkerLooper(), this::handleWorkerMessage);
+ mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
+ mInstantAppResolver = InstantAppResolver.newInstance(context);
+ mOnUpdateCallback = onUpdateCallback;
+
+ mShortcuts = new HashMap<>();
+ mInstantApps = new HashMap<>();
+ }
+
+ public void cacheItems(List shortcutKeys, List pkgNames) {
+ if (!shortcutKeys.isEmpty()) {
+ mWorker.removeMessages(BG_MSG_LOAD_SHORTCUTS);
+ Message.obtain(mWorker, BG_MSG_LOAD_SHORTCUTS, shortcutKeys).sendToTarget();
+ }
+ if (!pkgNames.isEmpty()) {
+ mWorker.removeMessages(BG_MSG_LOAD_INSTANT_APPS);
+ Message.obtain(mWorker, BG_MSG_LOAD_INSTANT_APPS, pkgNames).sendToTarget();
+ }
+ }
+
+ private boolean handleWorkerMessage(Message msg) {
+ switch (msg.what) {
+ case BG_MSG_LOAD_SHORTCUTS: {
+ List shortcutKeys = msg.obj != null ?
+ (List) msg.obj : Collections.EMPTY_LIST;
+ Map shortcutKeyAndInfos = new ArrayMap<>();
+ for (ShortcutKey shortcutKey : shortcutKeys) {
+ WorkspaceItemInfo workspaceItemInfo = loadShortcutWorker(shortcutKey);
+ if (workspaceItemInfo != null) {
+ shortcutKeyAndInfos.put(shortcutKey, workspaceItemInfo);
+ }
+ }
+ Message.obtain(mUiHandler, UI_MSG_UPDATE_SHORTCUTS, shortcutKeyAndInfos)
+ .sendToTarget();
+ return true;
+ }
+ case BG_MSG_LOAD_INSTANT_APPS: {
+ List pkgNames = msg.obj != null ?
+ (List) msg.obj : Collections.EMPTY_LIST;
+ List instantAppItemInfos = new ArrayList<>();
+ for (String pkgName : pkgNames) {
+ InstantAppItemInfo instantAppItemInfo = loadInstantApp(pkgName);
+ if (instantAppItemInfo != null) {
+ instantAppItemInfos.add(instantAppItemInfo);
+ }
+ }
+ Message.obtain(mUiHandler, UI_MSG_UPDATE_INSTANT_APPS, instantAppItemInfos)
+ .sendToTarget();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean handleUiMessage(Message msg) {
+ switch (msg.what) {
+ case UI_MSG_UPDATE_SHORTCUTS: {
+ mShortcuts.clear();
+ mShortcuts.putAll((Map) msg.obj);
+ mOnUpdateCallback.run();
+ return true;
+ }
+ case UI_MSG_UPDATE_INSTANT_APPS: {
+ List instantAppItemInfos = (List) msg.obj;
+ mInstantApps.clear();
+ for (InstantAppItemInfo instantAppItemInfo : instantAppItemInfos) {
+ mInstantApps.put(instantAppItemInfo.getTargetComponent().getPackageName(),
+ instantAppItemInfo);
+ }
+ mOnUpdateCallback.run();
+ if (DEBUG) {
+ Log.d(TAG, String.format("Cache size: %d, Cache: %s",
+ mInstantApps.size(), mInstantApps.toString()));
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @WorkerThread
+ private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) {
+ DeepShortcutManager mgr = DeepShortcutManager.getInstance(mContext);
+ List details = mgr.queryForFullDetails(
+ shortcutKey.componentName.getPackageName(),
+ Collections.singletonList(shortcutKey.getId()),
+ shortcutKey.user);
+ if (!details.isEmpty()) {
+ WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
+ try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
+ si.applyFrom(li.createShortcutIcon(details.get(0), true /* badged */, null));
+ } catch (Exception e) {
+ if (DEBUG) {
+ Log.e(TAG, "Error loading shortcut icon for " + shortcutKey.toString());
+ }
+ return null;
+ }
+ return si;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "No shortcut found: " + shortcutKey.toString());
+ }
+ return null;
+ }
+
+ private InstantAppItemInfo loadInstantApp(String pkgName) {
+ PackageManager pm = mContext.getPackageManager();
+
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0);
+ if (!mInstantAppResolver.isInstantApp(ai)) {
+ return null;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+
+ String url = retrieveDefaultUrl(pkgName, pm);
+ if (url == null) {
+ Log.w(TAG, "no default-url available for pkg " + pkgName);
+ return null;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .addCategory(Intent.CATEGORY_BROWSABLE)
+ .setData(Uri.parse(url));
+ InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName);
+ IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+ iconCache.getTitleAndIcon(info, false);
+ if (info.iconBitmap == null || iconCache.isDefaultIcon(info.iconBitmap, info.user)) {
+ return null;
+ }
+ return info;
+ }
+
+ @Nullable
+ public static String retrieveDefaultUrl(String pkgName, PackageManager pm) {
+ Intent mainIntent = new Intent().setAction(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_LAUNCHER).setPackage(pkgName);
+ List resolveInfos = pm.queryIntentActivities(
+ mainIntent, MATCH_INSTANT | PackageManager.GET_META_DATA);
+ String url = null;
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ if (resolveInfo.activityInfo.metaData != null
+ && resolveInfo.activityInfo.metaData.containsKey(DEFAULT_URL)) {
+ url = resolveInfo.activityInfo.metaData.getString(DEFAULT_URL);
+ }
+ }
+ return url;
+ }
+
+ @UiThread
+ public InstantAppItemInfo getInstantApp(String pkgName) {
+ return mInstantApps.get(pkgName);
+ }
+
+ @MainThread
+ public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
+ return mShortcuts.get(key);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e5f4617f4bfc9831840b18cc4e1f0bcb6189f8d
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.appprediction;
+
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.content.ComponentName;
+import android.content.Intent;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.WorkspaceItemInfo;
+
+public class InstantAppItemInfo extends AppInfo {
+
+ public InstantAppItemInfo(Intent intent, String packageName) {
+ this.intent = intent;
+ this.componentName = new ComponentName(packageName, COMPONENT_CLASS_MARKER);
+ }
+
+ @Override
+ public ComponentName getTargetComponent() {
+ return componentName;
+ }
+
+ @Override
+ public WorkspaceItemInfo makeWorkspaceItem() {
+ WorkspaceItemInfo workspaceItemInfo = super.makeWorkspaceItem();
+ workspaceItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ workspaceItemInfo.status = WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON
+ | WorkspaceItemInfo.FLAG_RESTORE_STARTED
+ | WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI;
+ workspaceItemInfo.intent.setPackage(componentName.getPackageName());
+ return workspaceItemInfo;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
new file mode 100644
index 0000000000000000000000000000000000000000..8f1282dedce133dd16d7567c353c0ecebfb13e49
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
@@ -0,0 +1,199 @@
+/**
+ * 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.InvariantDeviceProfile.CHANGE_FLAG_GRID;
+
+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.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.util.UiThreadHelper;
+
+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 {
+
+ 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;
+
+ protected final Context mContext;
+ private final Handler mMessageHandler;
+
+ // Accessed only on worker thread
+ private AppPredictor mHomeAppPredictor;
+ private AppPredictor mRecentsOverviewPredictor;
+
+ public PredictionAppTracker(Context context) {
+ mContext = context;
+ mMessageHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleMessage);
+ InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged);
+
+ mMessageHandler.sendEmptyMessage(MSG_INIT);
+ }
+
+ @UiThread
+ private void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+ if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
+ // Reinitialize everything
+ mMessageHandler.sendEmptyMessage(MSG_INIT);
+ }
+ }
+
+ @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)
+ .setExtras(getAppPredictionContextExtras(client))
+ .build());
+ predictor.registerPredictionUpdates(mContext.getMainExecutor(),
+ PredictionUiStateManager.INSTANCE.get(mContext).appPredictorCallback(client));
+ predictor.requestPredictionUpdate();
+ return predictor;
+ }
+
+ /**
+ * Override to add custom extras.
+ */
+ @WorkerThread
+ @Nullable
+ public Bundle getAppPredictionContextExtras(Client client){
+ return null;
+ }
+
+ @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 (mHomeAppPredictor != null) {
+ mHomeAppPredictor.notifyAppTargetEvent((AppTargetEvent) msg.obj);
+ }
+ return true;
+ }
+ case MSG_PREDICT: {
+ if (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));
+ }
+ }
+
+ @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), 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), cn.getPackageName(), user)
+ .setClassName(cn.getClassName())
+ .build();
+ sendLaunch(target, container);
+ }
+ }
+
+ @UiThread
+ private void sendLaunch(AppTarget target, String container) {
+ AppTargetEvent event = new AppTargetEvent.Builder(target, AppTargetEvent.ACTION_LAUNCH)
+ .setLaunchLocation(container == null ? CONTAINER_DEFAULT : container)
+ .build();
+ Message.obtain(mMessageHandler, MSG_LAUNCH, event).sendToTarget();
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb5cbddd4ad60dd09ae452320180785bdd8a6a2d
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -0,0 +1,384 @@
+/**
+ * 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.View;
+import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.allapps.FloatingHeaderRow;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.keyboard.FocusIndicatorHelper;
+import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
+import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.util.Themes;
+import com.android.quickstep.AnimatedFloat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+@TargetApi(Build.VERSION_CODES.P)
+public class PredictionRowView extends LinearLayout implements
+ LogContainerProvider, OnDeviceProfileChangeListener, FloatingHeaderRow {
+
+ private static final String TAG = "PredictionRowView";
+
+ private static final IntProperty TEXT_ALPHA =
+ new IntProperty("textAlpha") {
+ @Override
+ public void setValue(PredictionRowView view, int alpha) {
+ view.setTextAlpha(alpha);
+ }
+
+ @Override
+ public Integer get(PredictionRowView view) {
+ return view.mIconCurrentTextAlpha;
+ }
+ };
+
+ private static final Interpolator ALPHA_FACTOR_INTERPOLATOR =
+ (t) -> (t < 0.8f) ? 0 : (t - 0.8f) / 0.2f;
+
+ private static final OnClickListener PREDICTION_CLICK_LISTENER =
+ ItemClickHandler.getInstance(AppLaunchTracker.CONTAINER_PREDICTIONS);
+
+ private final Launcher mLauncher;
+ private final PredictionUiStateManager mPredictionUiStateManager;
+ private final int mNumPredictedAppsPerRow;
+
+ // The set of predicted app component names
+ private final List mPredictedAppComponents = new ArrayList<>();
+ // The set of predicted apps resolved from the component names and the current set of apps
+ private final ArrayList mPredictedApps = new ArrayList<>();
+ // Helper to drawing the focus indicator.
+ private final FocusIndicatorHelper mFocusHelper;
+
+ private final int mIconTextColor;
+ private final int mIconFullTextAlpha;
+ private int mIconCurrentTextAlpha;
+
+ private FloatingHeaderView mParent;
+ private boolean mScrolledOut;
+
+ private float mScrollTranslation = 0;
+ private final AnimatedFloat mContentAlphaFactor =
+ new AnimatedFloat(this::updateTranslationAndAlpha);
+ private final AnimatedFloat mOverviewScrollFactor =
+ new AnimatedFloat(this::updateTranslationAndAlpha);
+
+ private 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;
+ }
+
+ 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 mPredictionsEnabled;
+ }
+
+ /**
+ * Returns the predicted apps.
+ */
+ public List getPredictedApps() {
+ return mPredictedApps;
+ }
+
+ /**
+ * Sets the current set of predicted apps.
+ *
+ * This can be called before we get the full set of applications, we should merge the results
+ * only in onPredictionsUpdated() which is idempotent.
+ *
+ * If the number of predicted apps is the same as the previous list of predicted apps,
+ * we can optimize by swapping them in place.
+ */
+ public void setPredictedApps(List apps) {
+ mPredictedAppComponents.clear();
+ mPredictedAppComponents.addAll(apps);
+
+ mPredictedApps.clear();
+ mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
+ applyPredictionApps();
+ }
+
+ @Override
+ public void onDeviceProfileChanged(DeviceProfile dp) {
+ removeAllViews();
+ applyPredictionApps();
+ }
+
+ private void applyPredictionApps() {
+ 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);
+ }
+ }
+
+ boolean predictionsEnabled = predictionCount > 0;
+ if (predictionsEnabled != mPredictionsEnabled) {
+ mPredictionsEnabled = predictionsEnabled;
+ mLauncher.reapplyUi();
+ updateVisibility();
+ }
+ mParent.onHeightUpdated();
+ }
+
+ private List processPredictedAppComponents(List components) {
+ if (getAppsStore().getApps().isEmpty()) {
+ // Apps have not been bound yet.
+ return Collections.emptyList();
+ }
+
+ List predictedApps = new ArrayList<>();
+ for (ComponentKeyMapper mapper : components) {
+ ItemInfoWithIcon info = mapper.getApp(getAppsStore());
+ if (info != null) {
+ predictedApps.add(info);
+ } else {
+ if (FeatureFlags.IS_DOGFOOD_BUILD) {
+ Log.e(TAG, "Predicted app not found: " + mapper);
+ }
+ }
+ // Stop at the number of predicted apps
+ if (predictedApps.size() == mNumPredictedAppsPerRow) {
+ break;
+ }
+ }
+ return predictedApps;
+ }
+
+ @Override
+ public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+ LauncherLogProto.Target targetParent) {
+ for (int i = 0; i < mPredictedApps.size(); i++) {
+ ItemInfoWithIcon appInfo = mPredictedApps.get(i);
+ if (appInfo == info) {
+ targetParent.containerType = LauncherLogProto.ContainerType.PREDICTION;
+ target.predictedRank = i;
+ break;
+ }
+ }
+ }
+
+ public void setTextAlpha(int alpha) {
+ mIconCurrentTextAlpha = alpha;
+ int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha);
+ for (int i = 0; i < getChildCount(); i++) {
+ ((BubbleTextView) getChildAt(i)).setTextColor(iconColor);
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+
+ @Override
+ public void setVerticalScroll(int scroll, boolean isScrolledOut) {
+ mScrolledOut = isScrolledOut;
+ updateTranslationAndAlpha();
+ if (!isScrolledOut) {
+ mScrollTranslation = scroll;
+ updateTranslationAndAlpha();
+ }
+ }
+
+ private void updateTranslationAndAlpha() {
+ if (mPredictionsEnabled) {
+ setTranslationY((1 - mOverviewScrollFactor.value) * mScrollTranslation);
+
+ float factor = ALPHA_FACTOR_INTERPOLATOR.getInterpolation(mOverviewScrollFactor.value);
+ float endAlpha = factor + (1 - factor) * (mScrolledOut ? 0 : 1);
+ setAlpha(mContentAlphaFactor.value * endAlpha);
+ AlphaUpdateListener.updateVisibility(this);
+ }
+ }
+
+ @Override
+ public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
+ PropertySetter setter, Interpolator fadeInterpolator) {
+ boolean isDrawn = getAlpha() > 0;
+ int textAlpha = hasHeaderExtra
+ ? (hasContent ? mIconFullTextAlpha : 0) // Text follows the content visibility
+ : mIconCurrentTextAlpha; // Leave as before
+ if (!isDrawn) {
+ // If the header is not drawn, no need to animate the text alpha
+ setTextAlpha(textAlpha);
+ } else {
+ setter.setInt(this, TEXT_ALPHA, textAlpha, fadeInterpolator);
+ }
+
+ setter.setFloat(mOverviewScrollFactor, AnimatedFloat.VALUE,
+ (hasHeaderExtra && !hasContent) ? 1 : 0, LINEAR);
+ setter.setFloat(mContentAlphaFactor, AnimatedFloat.VALUE, hasHeaderExtra ? 1 : 0,
+ fadeInterpolator);
+ }
+
+ @Override
+ public void setInsets(Rect insets, DeviceProfile grid) {
+ int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
+ + grid.cellLayoutPaddingLeftRightPx;
+ setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return PredictionRowView.class;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..085bbc4a5eedb6216d2bd267a16c6f436191a81e
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -0,0 +1,326 @@
+/**
+ * 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.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,
+ OnIDPChangeListener, OnUpdateListener {
+
+ public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
+
+ // TODO (b/129421797): Update the client constants
+ public enum Client {
+ HOME("home"),
+ OVERVIEW("overview");
+
+ public final String id;
+
+ Client(String id) {
+ this.id = id;
+ }
+ }
+
+ public static final MainThreadInitializedObject INSTANCE =
+ new MainThreadInitializedObject<>(PredictionUiStateManager::new);
+
+ private final Context mContext;
+
+ private final DynamicItemCache mDynamicItemCache;
+ private final List[] mPredictionServicePredictions;
+
+ private int mMaxIconsPerRow;
+ private Client mActiveClient;
+
+ private AllAppsContainerView mAppsView;
+
+ private PredictionState mPendingState;
+ private PredictionState mCurrentState;
+
+ private boolean mGettingValidPredictionResults;
+
+ private PredictionUiStateManager(Context context) {
+ mContext = 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();
+ }
+ mGettingValidPredictionResults = Utilities.getDevicePrefs(context)
+ .getBoolean(LAST_PREDICTION_ENABLED_STATE, true);
+
+ // 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);
+ }
+ }
+
+ private void applyState(PredictionState state) {
+ mCurrentState = state;
+ if (mAppsView != null) {
+ mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
+ .setPredictedApps(mCurrentState.apps);
+ }
+ }
+
+ private void updatePredictionStateAfterCallback() {
+ boolean validResults = false;
+ for (List l : mPredictionServicePredictions) {
+ validResults |= l != null && !l.isEmpty();
+ }
+ if (validResults != mGettingValidPredictionResults) {
+ mGettingValidPredictionResults = validResults;
+ Utilities.getDevicePrefs(mContext).edit()
+ .putBoolean(LAST_PREDICTION_ENABLED_STATE, true)
+ .apply();
+ }
+ dispatchOnChange(true);
+ }
+
+ public AppPredictor.Callback appPredictorCallback(Client client) {
+ return targets -> {
+ mPredictionServicePredictions[client.ordinal()] = targets;
+ updatePredictionStateAfterCallback();
+ };
+ }
+
+ 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 = mGettingValidPredictionResults;
+ if (!state.isEnabled) {
+ state.apps = Collections.EMPTY_LIST;
+ return state;
+ }
+
+ state.apps = new ArrayList<>();
+
+ List appTargets = mPredictionServicePredictions[mActiveClient.ordinal()];
+ if (!appTargets.isEmpty()) {
+ for (AppTarget appTarget : appTargets) {
+ ComponentKey key;
+ if (appTarget.getShortcutInfo() != null) {
+ key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
+ } else {
+ key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
+ appTarget.getClassName()), appTarget.getUser());
+ }
+ state.apps.add(new ComponentKeyMapper(mContext, key, mDynamicItemCache));
+ }
+ }
+ updateDependencies(state);
+ return state;
+ }
+
+ private void updateDependencies(PredictionState state) {
+ if (!state.isEnabled || mAppsView == null) {
+ return;
+ }
+
+ IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+ List instantAppsToLoad = new ArrayList<>();
+ List shortcutsToLoad = new ArrayList<>();
+ int total = state.apps.size();
+ for (int i = 0, count = 0; i < total && count < mMaxIconsPerRow; i++) {
+ ComponentKeyMapper mapper = state.apps.get(i);
+ // Update instant apps
+ if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
+ instantAppsToLoad.add(mapper.getPackage());
+ count++;
+ } else if (mapper.getComponentKey() instanceof ShortcutKey) {
+ shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
+ count++;
+ } else {
+ // Reload high res icon
+ AppInfo info = (AppInfo) mapper.getApp(mAppsView.getAppsStore());
+ if (info != null) {
+ if (info.usingLowResIcon()) {
+ // TODO: Update icon cache to support null callbacks.
+ iconCache.updateIconInBackground(this, info);
+ }
+ count++;
+ }
+ }
+ }
+ mDynamicItemCache.cacheItems(shortcutsToLoad, instantAppsToLoad);
+ }
+
+ @Override
+ public void onAppsUpdated() {
+ dispatchOnChange(false);
+ }
+
+ private boolean canApplyPredictions(PredictionState newState) {
+ if (mAppsView == null) {
+ // If there is no apps view, no need to schedule.
+ return true;
+ }
+ Launcher launcher = Launcher.getLauncher(mAppsView.getContext());
+ PredictionRowView predictionRow = mAppsView.getFloatingHeaderView().
+ findFixedRowByType(PredictionRowView.class);
+ if (!predictionRow.isShown() || predictionRow.getAlpha() == 0 ||
+ launcher.isForceInvisible()) {
+ return true;
+ }
+
+ if (mCurrentState.isEnabled != newState.isEnabled
+ || mCurrentState.apps.isEmpty() != newState.apps.isEmpty()) {
+ // If the visibility of the prediction row is changing, apply immediately.
+ return true;
+ }
+
+ if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+ // If we are here & mAppsView.isShown() = true, we are probably in all-apps or mid way
+ return false;
+ }
+ if (!launcher.isInState(OVERVIEW) && !launcher.isInState(BACKGROUND_APP)) {
+ // Just a fallback as we dont need to apply instantly, if we are not in the swipe-up UI
+ return false;
+ }
+
+ // Instead of checking against 1, we should check against (1 + delta), where delta accounts
+ // for the nav-bar height (as app icon can still be visible under the nav-bar). Checking
+ // against 1, keeps the logic simple :)
+ return launcher.getAllAppsController().getProgress() > 1;
+ }
+
+ public PredictionState getCurrentState() {
+ return mCurrentState;
+ }
+
+ public static class PredictionState {
+
+ public boolean isEnabled;
+ public List apps;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..a3c2e3cc815e97b9f4cf636a6eb8d2c820030fa9
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
@@ -0,0 +1,223 @@
+/*
+ * 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.uioverrides;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.Gravity;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager.StateHandler;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
+import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.OverviewToAllAppsTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.ArrayList;
+
+/**
+ * Provides recents-related {@link UiFactory} logic and classes.
+ */
+public abstract class RecentsUiFactory {
+
+ public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
+ private static final AsyncCommand SET_SHELF_HEIGHT_CMD = (visible, height) ->
+ WindowManagerWrapper.getInstance().setShelfHeight(visible != 0, height);
+
+ public static RotationMode ROTATION_LANDSCAPE = new RotationMode(-90) {
+ @Override
+ public void mapRect(int left, int top, int right, int bottom, Rect out) {
+ out.left = top;
+ out.top = right;
+ out.right = bottom;
+ out.bottom = left;
+ }
+
+ @Override
+ public void mapInsets(Context context, Rect insets, Rect out) {
+ // If there is a display cutout, the top insets in portrait would also include the
+ // cutout, which we will get as the left inset in landscape. Using the max of left and
+ // top allows us to cover both cases (with or without cutout).
+ if (SysUINavigationMode.getMode(context) == NO_BUTTON) {
+ out.top = Math.max(insets.top, insets.left);
+ out.bottom = Math.max(insets.right, insets.bottom);
+ out.left = out.right = 0;
+ } else {
+ out.top = Math.max(insets.top, insets.left);
+ out.bottom = insets.right;
+ out.left = insets.bottom;
+ out.right = 0;
+ }
+ }
+ };
+
+ public static RotationMode ROTATION_SEASCAPE = new RotationMode(90) {
+ @Override
+ public void mapRect(int left, int top, int right, int bottom, Rect out) {
+ out.left = bottom;
+ out.top = left;
+ out.right = top;
+ out.bottom = right;
+ }
+
+ @Override
+ public void mapInsets(Context context, Rect insets, Rect out) {
+ if (SysUINavigationMode.getMode(context) == NO_BUTTON) {
+ out.top = Math.max(insets.top, insets.right);
+ out.bottom = Math.max(insets.left, insets.bottom);
+ out.left = out.right = 0;
+ } else {
+ out.top = Math.max(insets.top, insets.right);
+ out.bottom = insets.left;
+ out.right = insets.bottom;
+ out.left = 0;
+ }
+ }
+
+ @Override
+ public int toNaturalGravity(int absoluteGravity) {
+ int horizontalGravity = absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ int verticalGravity = absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ if (horizontalGravity == Gravity.RIGHT) {
+ horizontalGravity = Gravity.LEFT;
+ } else if (horizontalGravity == Gravity.LEFT) {
+ horizontalGravity = Gravity.RIGHT;
+ }
+
+ if (verticalGravity == Gravity.TOP) {
+ verticalGravity = Gravity.BOTTOM;
+ } else if (verticalGravity == Gravity.BOTTOM) {
+ verticalGravity = Gravity.TOP;
+ }
+
+ return ((absoluteGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK)
+ & ~Gravity.VERTICAL_GRAVITY_MASK)
+ | horizontalGravity | verticalGravity;
+ }
+ };
+
+ public static RotationMode getRotationMode(DeviceProfile dp) {
+ return !dp.isVerticalBarLayout() ? RotationMode.NORMAL
+ : (dp.isSeascape() ? ROTATION_SEASCAPE : ROTATION_LANDSCAPE);
+ }
+
+ public static TouchController[] createTouchControllers(Launcher launcher) {
+ Mode mode = SysUINavigationMode.getMode(launcher);
+
+ ArrayList list = new ArrayList<>();
+ list.add(launcher.getDragController());
+ if (mode == NO_BUTTON) {
+ list.add(new QuickSwitchTouchController(launcher));
+ list.add(new NavBarToHomeTouchController(launcher));
+ list.add(new FlingAndHoldTouchController(launcher));
+ } else {
+ if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+ list.add(new OverviewToAllAppsTouchController(launcher));
+ list.add(new LandscapeEdgeSwipeController(launcher));
+ if (mode.hasGestures) {
+ list.add(new TransposedQuickSwitchTouchController(launcher));
+ }
+ } else {
+ list.add(new PortraitStatesTouchController(launcher,
+ mode.hasGestures /* allowDragToOverview */));
+ if (mode.hasGestures) {
+ list.add(new QuickSwitchTouchController(launcher));
+ }
+ }
+ }
+
+ list.add(new LauncherTaskViewController(launcher));
+ return list.toArray(new TouchController[list.size()]);
+ }
+
+ /**
+ * Creates and returns the controller responsible for recents view state transitions.
+ *
+ * @param launcher the launcher activity
+ * @return state handler for recents
+ */
+ public static StateHandler createRecentsViewStateController(Launcher launcher) {
+ return new RecentsViewStateController(launcher);
+ }
+
+ /**
+ * Clears the swipe shared state for the current swipe gesture.
+ */
+ public static void clearSwipeSharedState(boolean finishAnimation) {
+ TouchInteractionService.getSwipeSharedState().clearAllState(finishAnimation);
+ }
+
+ /**
+ * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
+ *
+ * @param launcher the launcher activity
+ */
+ public static void onLauncherStateOrResumeChanged(Launcher launcher) {
+ LauncherState state = launcher.getStateManager().getState();
+ DeviceProfile profile = launcher.getDeviceProfile();
+ boolean visible = (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
+ && !profile.isVerticalBarLayout();
+ UiThreadHelper.runAsyncCommand(launcher, SET_SHELF_HEIGHT_CMD,
+ visible ? 1 : 0, profile.hotseatBarSizePx);
+
+ if (state == NORMAL) {
+ launcher.getOverviewPanel().setSwipeDownShouldLaunchApp(false);
+ }
+ }
+
+ private static final class LauncherTaskViewController extends
+ TaskViewTouchController {
+
+ LauncherTaskViewController(Launcher activity) {
+ super(activity);
+ }
+
+ @Override
+ protected boolean isRecentsInteractive() {
+ return mActivity.isInState(OVERVIEW);
+ }
+
+ @Override
+ protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
+ mActivity.getStateManager().setCurrentUserControlledAnimation(animController);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5d84247a88c85ae94ace04ac6775152a03a0e1c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.uioverrides;
+
+import static com.android.launcher3.LauncherState.RECENTS_CLEAR_ALL_BUTTON;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
+import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+
+import android.animation.ValueAnimator;
+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;
+
+/**
+ * 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.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public final class RecentsViewStateController extends
+ BaseRecentsViewStateController {
+
+ public RecentsViewStateController(Launcher launcher) {
+ super(launcher);
+ }
+
+ @Override
+ public void setState(@NonNull LauncherState state) {
+ super.setState(state);
+ if (state.overviewUi) {
+ mRecentsView.updateEmptyMessage();
+ mRecentsView.resetTaskVisuals();
+ }
+ setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state.getVisibleElements(mLauncher));
+ mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
+ }
+
+ @Override
+ void setStateWithAnimationInternal(@NonNull final LauncherState toState,
+ @NonNull AnimatorSetBuilder builder, @NonNull AnimationConfig config) {
+ super.setStateWithAnimationInternal(toState, builder, config);
+
+ if (!toState.overviewUi) {
+ builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals);
+ }
+
+ if (toState.overviewUi) {
+ ValueAnimator updateAnim = ValueAnimator.ofFloat(0, 1);
+ updateAnim.addUpdateListener(valueAnimator -> {
+ // While animating into recents, update the visible task data as needed
+ mRecentsView.loadVisibleTaskData();
+ });
+ updateAnim.setDuration(config.duration);
+ builder.play(updateAnim);
+ mRecentsView.updateEmptyMessage();
+ }
+
+ PropertySetter propertySetter = config.getPropertySetter(builder);
+ setAlphas(propertySetter, toState.getVisibleElements(mLauncher));
+ float fullscreenProgress = toState.getOverviewFullscreenProgress();
+ propertySetter.setFloat(mRecentsView, FULLSCREEN_PROGRESS, fullscreenProgress, LINEAR);
+ }
+
+ 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
+ FloatProperty getContentAlphaProperty() {
+ return CONTENT_ALPHA;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ee08c12d2762d057f03b76e5a7713c0c1e3b8e3
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.uioverrides.states;
+
+import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * State indicating that the Launcher is behind an app
+ */
+public class BackgroundAppState extends OverviewState {
+
+ private static final int STATE_FLAGS =
+ FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY
+ | FLAG_DISABLE_INTERACTION;
+
+ public BackgroundAppState(int id) {
+ this(id, LauncherLogProto.ContainerType.TASKSWITCHER);
+ }
+
+ protected BackgroundAppState(int id, int logContainer) {
+ super(id, logContainer, OVERVIEW_TRANSITION_MS, STATE_FLAGS);
+ }
+
+ @Override
+ public void onStateEnabled(Launcher launcher) {
+ AbstractFloatingView.closeAllOpenViews(launcher, false);
+ }
+
+ @Override
+ public float getVerticalProgress(Launcher launcher) {
+ if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+ return super.getVerticalProgress(launcher);
+ }
+ int transitionLength = LayoutUtils.getShelfTrackingDistance(launcher,
+ launcher.getDeviceProfile());
+ AllAppsTransitionController controller = launcher.getAllAppsController();
+ float scrollRange = Math.max(controller.getShiftRange(), 1);
+ float progressDelta = (transitionLength / scrollRange);
+ return super.getVerticalProgress(launcher) + progressDelta;
+ }
+
+ @Override
+ public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
+ // Initialize the recents view scale to what it would be when starting swipe up
+ RecentsView recentsView = launcher.getOverviewPanel();
+ int taskCount = recentsView.getTaskViewCount();
+ if (taskCount == 0) {
+ return super.getOverviewScaleAndTranslation(launcher);
+ }
+ TaskView dummyTask = recentsView.getTaskViewAt(Math.max(taskCount - 1,
+ recentsView.getCurrentPage()));
+ return recentsView.getTempClipAnimationHelper().updateForFullscreenOverview(dummyTask)
+ .getScaleAndTranslation();
+ }
+
+ @Override
+ public float getOverviewFullscreenProgress() {
+ return 1;
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return super.getVisibleElements(launcher)
+ & ~RECENTS_CLEAR_ALL_BUTTON & ~VERTICAL_SWIPE_INDICATOR;
+ }
+
+ @Override
+ public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
+ if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
+ // Translate hotseat offscreen if we show it in overview.
+ ScaleAndTranslation scaleAndTranslation = super.getHotseatScaleAndTranslation(launcher);
+ scaleAndTranslation.translationY = LayoutUtils.getShelfTrackingDistance(launcher,
+ launcher.getDeviceProfile());
+ return scaleAndTranslation;
+ }
+ return super.getHotseatScaleAndTranslation(launcher);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
new file mode 100644
index 0000000000000000000000000000000000000000..c954762837dae068b668ad0e0eb2ab65f55131e3
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
@@ -0,0 +1,48 @@
+/*
+ * 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.uioverrides.states;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+
+public class OverviewPeekState extends OverviewState {
+ public OverviewPeekState(int id) {
+ super(id);
+ }
+
+ @Override
+ public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
+ ScaleAndTranslation result = super.getOverviewScaleAndTranslation(launcher);
+ result.translationX = NORMAL.getOverviewScaleAndTranslation(launcher).translationX
+ - launcher.getResources().getDimension(R.dimen.overview_peek_distance);
+ return result;
+ }
+
+ @Override
+ public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
+ AnimatorSetBuilder builder) {
+ if (this == OVERVIEW_PEEK && fromState == NORMAL) {
+ builder.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
+ builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
new file mode 100644
index 0000000000000000000000000000000000000000..5543860eeceb6c266f3d331e827ded3c66870c7f
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -0,0 +1,215 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.uioverrides.states;
+
+import static android.view.View.VISIBLE;
+
+import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.uioverrides.UiFactory;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Definition for overview state
+ */
+public class OverviewState extends LauncherState {
+
+ // Scale recents takes before animating in
+ private static final float RECENTS_PREPARE_SCALE = 1.33f;
+
+ protected static final Rect sTempRect = new Rect();
+
+ private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED
+ | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY;
+
+ public OverviewState(int id) {
+ this(id, OVERVIEW_TRANSITION_MS, STATE_FLAGS);
+ }
+
+ protected OverviewState(int id, int transitionDuration, int stateFlags) {
+ this(id, ContainerType.TASKSWITCHER, transitionDuration, stateFlags);
+ }
+
+ protected OverviewState(int id, int logContainer, int transitionDuration, int stateFlags) {
+ super(id, logContainer, transitionDuration, stateFlags);
+ }
+
+ @Override
+ public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
+ RecentsView recentsView = launcher.getOverviewPanel();
+ Workspace workspace = launcher.getWorkspace();
+ View workspacePage = workspace.getPageAt(workspace.getCurrentPage());
+ float workspacePageWidth = workspacePage != null && workspacePage.getWidth() != 0
+ ? workspacePage.getWidth() : launcher.getDeviceProfile().availableWidthPx;
+ recentsView.getTaskSize(sTempRect);
+ float scale = (float) sTempRect.width() / workspacePageWidth;
+ float parallaxFactor = 0.5f;
+ 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);
+ }
+
+ @Override
+ public void onStateEnabled(Launcher launcher) {
+ AbstractFloatingView.closeAllOpenViews(launcher);
+ }
+
+ @Override
+ public void onStateTransitionEnd(Launcher launcher) {
+ launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
+ DiscoveryBounce.showForOverviewIfNeeded(launcher);
+ }
+
+ @Override
+ public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
+ return new PageAlphaProvider(DEACCEL_2) {
+ @Override
+ public float getPageAlpha(int pageIndex) {
+ return 0;
+ }
+ };
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+ return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON;
+ } else {
+ return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON |
+ (launcher.getAppsView().getFloatingHeaderView().hasVisibleContent()
+ ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
+ }
+ }
+
+ @Override
+ public float getWorkspaceScrimAlpha(Launcher launcher) {
+ return 0.5f;
+ }
+
+ @Override
+ public float getVerticalProgress(Launcher launcher) {
+ if ((getVisibleElements(launcher) & ALL_APPS_HEADER_EXTRA) == 0) {
+ // We have no all apps content, so we're still at the fully down progress.
+ return super.getVerticalProgress(launcher);
+ }
+ return getDefaultVerticalProgress(launcher);
+ }
+
+ public static float getDefaultVerticalProgress(Launcher launcher) {
+ return 1 - (getDefaultSwipeHeight(launcher)
+ / launcher.getAllAppsController().getShiftRange());
+ }
+
+ @Override
+ public String getDescription(Launcher launcher) {
+ return launcher.getString(R.string.accessibility_desc_recent_apps);
+ }
+
+ public static float getDefaultSwipeHeight(Launcher launcher) {
+ return getDefaultSwipeHeight(launcher.getDeviceProfile());
+ }
+
+ public static float getDefaultSwipeHeight(DeviceProfile dp) {
+ return dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx;
+ }
+
+ @Override
+ public void onBackPressed(Launcher launcher) {
+ TaskView taskView = launcher.getOverviewPanel().getRunningTaskView();
+ if (taskView != null) {
+ launcher.getUserEventDispatcher().logActionCommand(Action.Command.BACK,
+ newContainerTarget(ContainerType.OVERVIEW));
+ taskView.launchTask(true);
+ } else {
+ super.onBackPressed(launcher);
+ }
+ }
+
+ @Override
+ public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
+ AnimatorSetBuilder builder) {
+ if (fromState == NORMAL && this == OVERVIEW) {
+ if (SysUINavigationMode.getMode(launcher) == SysUINavigationMode.Mode.NO_BUTTON) {
+ builder.setInterpolator(ANIM_WORKSPACE_SCALE, ACCEL);
+ builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
+ } else {
+ builder.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
+
+ // Scale up the recents, if it is not coming from the side
+ RecentsView overview = launcher.getOverviewPanel();
+ if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
+ SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
+ }
+ }
+ builder.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
+ builder.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
+ builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
+ builder.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
+ }
+ }
+
+ public static OverviewState newBackgroundState(int id) {
+ return new BackgroundAppState(id);
+ }
+
+ public static OverviewState newPeekState(int id) {
+ return new OverviewPeekState(id);
+ }
+
+ public static OverviewState newSwitchState(int id) {
+ return new QuickSwitchState(id);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c9f46fc4700198dcc8dd3b4d791cfc4cacf30b1
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -0,0 +1,68 @@
+/*
+ * 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.uioverrides.states;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * State to indicate we are about to launch a recent task. Note that this state is only used when
+ * quick switching from launcher; quick switching from an app uses WindowTransformSwipeHelper.
+ * @see com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget#NEW_TASK
+ */
+public class QuickSwitchState extends BackgroundAppState {
+
+ private static final String TAG = "QuickSwitchState";
+
+ public QuickSwitchState(int id) {
+ super(id, LauncherLogProto.ContainerType.APP);
+ }
+
+ @Override
+ public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
+ float shiftRange = launcher.getAllAppsController().getShiftRange();
+ float shiftProgress = getVerticalProgress(launcher) - NORMAL.getVerticalProgress(launcher);
+ float translationY = shiftProgress * shiftRange;
+ return new ScaleAndTranslation(1, 0, translationY);
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return NONE;
+ }
+
+ @Override
+ public void onStateTransitionEnd(Launcher launcher) {
+ TaskView tasktolaunch = launcher.getOverviewPanel().getTaskViewAt(0);
+ if (tasktolaunch != null) {
+ tasktolaunch.launchTask(false, success -> {
+ if (!success) {
+ launcher.getStateManager().goToState(OVERVIEW);
+ tasktolaunch.notifyTaskLaunchFailed(TAG);
+ } else {
+ launcher.getStateManager().moveToRestState();
+ }
+ }, new Handler(Looper.getMainLooper()));
+ } else {
+ launcher.getStateManager().goToState(NORMAL);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fe4bcfd9c12cab9b5f9cb0c81a75a2f5093d893
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
@@ -0,0 +1,178 @@
+/*
+ * 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.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
+import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
+import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_PEEK_COMPONENT;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_SCALE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_TRANSLATE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+
+import android.animation.Animator;
+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;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.quickstep.OverviewInteractionState;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Touch controller which handles swipe and hold to go to Overview
+ */
+public class FlingAndHoldTouchController extends PortraitStatesTouchController {
+
+ private static final long PEEK_IN_ANIM_DURATION = 240;
+ private static final long PEEK_OUT_ANIM_DURATION = 100;
+ private static final float MAX_DISPLACEMENT_PERCENT = 0.75f;
+
+ private final MotionPauseDetector mMotionPauseDetector;
+ private final float mMotionPauseMinDisplacement;
+ private final float mMotionPauseMaxDisplacement;
+
+ private AnimatorSet mPeekAnim;
+
+ public FlingAndHoldTouchController(Launcher l) {
+ super(l, false /* allowDragToOverview */);
+ mMotionPauseDetector = new MotionPauseDetector(l);
+ mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
+ mMotionPauseMaxDisplacement = getShiftRange() * MAX_DISPLACEMENT_PERCENT;
+ }
+
+ @Override
+ protected long getAtomicDuration() {
+ return 300;
+ }
+
+ @Override
+ public void onDragStart(boolean start) {
+ mMotionPauseDetector.clear();
+
+ super.onDragStart(start);
+
+ if (handlingOverviewAnim()) {
+ mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ recentsView.setOverviewStateEnabled(isPaused);
+ if (mPeekAnim != null) {
+ mPeekAnim.cancel();
+ }
+ LauncherState fromState = isPaused ? NORMAL : OVERVIEW_PEEK;
+ LauncherState toState = isPaused ? OVERVIEW_PEEK : NORMAL;
+ long peekDuration = isPaused ? PEEK_IN_ANIM_DURATION : PEEK_OUT_ANIM_DURATION;
+ mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(fromState, toState,
+ new AnimatorSetBuilder(), ATOMIC_OVERVIEW_PEEK_COMPONENT, peekDuration);
+ mPeekAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPeekAnim = null;
+ }
+ });
+ mPeekAnim.start();
+ recentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ });
+ }
+ }
+
+ /**
+ * @return Whether we are handling the overview animation, rather than
+ * having it as part of the existing animation to the target state.
+ */
+ private boolean handlingOverviewAnim() {
+ int stateFlags = OverviewInteractionState.INSTANCE.get(mLauncher).getSystemUiStateFlags();
+ return mStartState == NORMAL && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
+ }
+
+ @Override
+ protected AnimatorSetBuilder getAnimatorSetBuilderForStates(LauncherState fromState,
+ LauncherState toState) {
+ if (fromState == NORMAL && toState == ALL_APPS) {
+ AnimatorSetBuilder builder = new AnimatorSetBuilder();
+
+ // Get workspace out of the way quickly, to prepare for potential pause.
+ builder.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL_3);
+ builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, DEACCEL_3);
+ builder.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_3);
+ return builder;
+ }
+ return super.getAnimatorSetBuilderForStates(fromState, toState);
+ }
+
+ @Override
+ public boolean onDrag(float displacement, MotionEvent event) {
+ float upDisplacement = -displacement;
+ mMotionPauseDetector.setDisallowPause(upDisplacement < mMotionPauseMinDisplacement
+ || upDisplacement > mMotionPauseMaxDisplacement);
+ mMotionPauseDetector.addPosition(displacement, event.getEventTime());
+ return super.onDrag(displacement, event);
+ }
+
+ @Override
+ public void onDragEnd(float velocity, boolean fling) {
+ if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) {
+ if (mPeekAnim != null) {
+ mPeekAnim.cancel();
+ }
+
+ AnimatorSetBuilder builder = new AnimatorSetBuilder();
+ builder.setInterpolator(ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
+ if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+ builder.setInterpolator(ANIM_HOTSEAT_SCALE, OVERSHOOT_1_2);
+ builder.setInterpolator(ANIM_HOTSEAT_TRANSLATE, OVERSHOOT_1_2);
+ }
+ AnimatorSet overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
+ NORMAL, OVERVIEW, builder, ANIM_ALL, ATOMIC_DURATION);
+ overviewAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
+ }
+ });
+ overviewAnim.start();
+ } else {
+ super.onDragEnd(velocity, fling);
+ }
+ mMotionPauseDetector.clear();
+ }
+
+ @Override
+ protected void updateAnimatorBuilderOnReinit(AnimatorSetBuilder builder) {
+ if (handlingOverviewAnim()) {
+ // We don't want the state transition to all apps to animate overview,
+ // as that will cause a jump after our atomic animation.
+ builder.addFlag(AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
new file mode 100644
index 0000000000000000000000000000000000000000..d66af1ae24f7582b85178bab6ce92b528af13dc4
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -0,0 +1,222 @@
+/*
+ * 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.uioverrides.touchcontrollers;
+
+import static android.view.View.TRANSLATION_X;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.view.MotionEvent;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager.AnimationConfig;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+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.Touch;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
+ */
+public class NavBarToHomeTouchController implements TouchController, SwipeDetector.Listener {
+
+ private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
+
+ private final Launcher mLauncher;
+ private final SwipeDetector mSwipeDetector;
+ private final float mPullbackDistance;
+
+ private boolean mNoIntercept;
+ private LauncherState mStartState;
+ private LauncherState mEndState = NORMAL;
+ private AnimatorPlaybackController mCurrentAnimation;
+
+ public NavBarToHomeTouchController(Launcher launcher) {
+ mLauncher = launcher;
+ mSwipeDetector = new SwipeDetector(mLauncher, this, SwipeDetector.VERTICAL);
+ mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance);
+ }
+
+ @Override
+ public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mStartState = mLauncher.getStateManager().getState();
+ mNoIntercept = !canInterceptTouch(ev);
+ if (mNoIntercept) {
+ return false;
+ }
+ mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+ }
+
+ if (mNoIntercept) {
+ return false;
+ }
+
+ onControllerTouchEvent(ev);
+ return mSwipeDetector.isDraggingOrSettling();
+ }
+
+ private boolean canInterceptTouch(MotionEvent ev) {
+ boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
+ if (!cameFromNavBar) {
+ return false;
+ }
+ if (mStartState == OVERVIEW || mStartState == ALL_APPS) {
+ return true;
+ }
+ if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public final boolean onControllerTouchEvent(MotionEvent ev) {
+ return mSwipeDetector.onTouchEvent(ev);
+ }
+
+ private float getShiftRange() {
+ return mLauncher.getDeviceProfile().heightPx;
+ }
+
+ @Override
+ public void onDragStart(boolean start) {
+ initCurrentAnimation();
+ }
+
+ private void initCurrentAnimation() {
+ long accuracy = (long) (getShiftRange() * 2);
+ final AnimatorSet anim = new AnimatorSet();
+ if (mStartState == OVERVIEW) {
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ float pullbackDist = mPullbackDistance;
+ if (!recentsView.isRtl()) {
+ pullbackDist = -pullbackDist;
+ }
+ Animator pullback = ObjectAnimator.ofFloat(recentsView, TRANSLATION_X, pullbackDist);
+ pullback.setInterpolator(PULLBACK_INTERPOLATOR);
+ anim.play(pullback);
+ } else if (mStartState == ALL_APPS) {
+ AnimatorSetBuilder builder = new AnimatorSetBuilder();
+ AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
+ Animator allAppsProgress = ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS,
+ -mPullbackDistance / allAppsController.getShiftRange());
+ allAppsProgress.setInterpolator(PULLBACK_INTERPOLATOR);
+ builder.play(allAppsProgress);
+ // Slightly fade out all apps content to further distinguish from scrolling.
+ builder.setInterpolator(AnimatorSetBuilder.ANIM_ALL_APPS_FADE, Interpolators
+ .mapToProgress(PULLBACK_INTERPOLATOR, 0, 0.5f));
+ AnimationConfig config = new AnimationConfig();
+ config.duration = accuracy;
+ allAppsController.setAlphas(mEndState.getVisibleElements(mLauncher), config, builder);
+ anim.play(builder.build());
+ }
+ AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mLauncher);
+ if (topView != null) {
+ Animator hintCloseAnim = topView.createHintCloseAnim(mPullbackDistance);
+ if (hintCloseAnim != null) {
+ hintCloseAnim.setInterpolator(PULLBACK_INTERPOLATOR);
+ anim.play(hintCloseAnim);
+ }
+ }
+ anim.setDuration(accuracy);
+ mCurrentAnimation = AnimatorPlaybackController.wrap(anim, accuracy, this::clearState);
+ }
+
+ private void clearState() {
+ mCurrentAnimation = null;
+ mSwipeDetector.finishedScrolling();
+ mSwipeDetector.setDetectableScrollConditions(0, false);
+ }
+
+ @Override
+ public boolean onDrag(float displacement) {
+ // Only allow swipe up.
+ displacement = Math.min(0, displacement);
+ float progress = Utilities.getProgress(displacement, 0, getShiftRange());
+ mCurrentAnimation.setPlayFraction(progress);
+ return true;
+ }
+
+ @Override
+ public void onDragEnd(float velocity, boolean fling) {
+ final int logAction = fling ? Touch.FLING : Touch.SWIPE;
+ float progress = mCurrentAnimation.getProgressFraction();
+ float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(progress);
+ boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
+ || (velocity < 0 && fling);
+ if (success) {
+ mLauncher.getStateManager().goToState(mEndState, true,
+ () -> onSwipeInteractionCompleted(mEndState));
+ if (mStartState != mEndState) {
+ logStateChange(mStartState.containerType, logAction);
+ }
+ AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(mLauncher);
+ if (topOpenView != null) {
+ AbstractFloatingView.closeAllOpenViews(mLauncher);
+ logStateChange(topOpenView.getLogContainerType(), logAction);
+ }
+ } else {
+ // Quickly return to the state we came from (we didn't move far).
+ ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+ anim.setFloatValues(progress, 0);
+ anim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ onSwipeInteractionCompleted(mStartState);
+ }
+ });
+ anim.setDuration(80).start();
+ }
+ }
+
+ 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,
+ mLauncher.getWorkspace().getCurrentPage());
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/OverviewToAllAppsTouchController.java
similarity index 92%
rename from quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java
rename to quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/OverviewToAllAppsTouchController.java
index 0f9b57f033b61e0f1831aaaa931231d7b75c6d16..73f328bc1dfc058c2ac2a66276a5a327e2d980ed 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/OverviewToAllAppsTouchController.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.launcher3.uioverrides;
+package com.android.launcher3.uioverrides.touchcontrollers;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
@@ -24,6 +24,7 @@ import android.view.MotionEvent;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.quickstep.TouchInteractionService;
import com.android.quickstep.views.RecentsView;
@@ -36,7 +37,7 @@ import com.android.quickstep.views.RecentsView;
public class OverviewToAllAppsTouchController extends PortraitStatesTouchController {
public OverviewToAllAppsTouchController(Launcher l) {
- super(l);
+ super(l, true /* allowDragToOverview */);
}
@Override
@@ -52,7 +53,7 @@ public class OverviewToAllAppsTouchController extends PortraitStatesTouchControl
// In all-apps only listen if the container cannot scroll itself
return mLauncher.getAppsView().shouldContainerScroll(ev);
} else if (mLauncher.isInState(NORMAL)) {
- return true;
+ return (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0;
} else if (mLauncher.isInState(OVERVIEW)) {
RecentsView rv = mLauncher.getOverviewPanel();
return ev.getY() > (rv.getBottom() - rv.getPaddingBottom());
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..20a2487349998b1ed3734bbf6773c10d69aaa70f
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -0,0 +1,85 @@
+/*
+ * 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.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController.isTouchOverHotseat;
+
+import android.view.MotionEvent;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.util.PendingAnimation;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Helper class for {@link PortraitStatesTouchController} that determines swipeable regions and
+ * animations on the overview state that depend on the recents implementation.
+ */
+public final class PortraitOverviewStateTouchHelper {
+
+ RecentsView mRecentsView;
+ Launcher mLauncher;
+
+ public PortraitOverviewStateTouchHelper(Launcher launcher) {
+ mLauncher = launcher;
+ mRecentsView = launcher.getOverviewPanel();
+ }
+
+ /**
+ * Whether or not {@link PortraitStatesTouchController} should intercept the touch when on the
+ * overview state.
+ *
+ * @param ev the motion event
+ * @return true if we should intercept the motion event
+ */
+ boolean canInterceptTouch(MotionEvent ev) {
+ if (mRecentsView.getChildCount() > 0) {
+ // Allow swiping up in the gap between the hotseat and overview.
+ return ev.getY() >= mRecentsView.getChildAt(0).getBottom();
+ } else {
+ // If there are no tasks, we only intercept if we're below the hotseat height.
+ return isTouchOverHotseat(mLauncher, ev);
+ }
+ }
+
+ /**
+ * Whether or not swiping down to leave overview state should return to the currently running
+ * task app.
+ *
+ * @return true if going back should take the user to the currently running task
+ */
+ boolean shouldSwipeDownReturnToApp() {
+ TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getNextPage());
+ return taskView != null && mRecentsView.shouldSwipeDownLaunchApp();
+ }
+
+ /**
+ * Create the animation for going from overview to the task app via swiping. Should only be
+ * called when {@link #shouldSwipeDownReturnToApp()} returns true.
+ *
+ * @param duration how long the animation should be
+ * @return the animation
+ */
+ PendingAnimation createSwipeDownToTaskAppAnimation(long duration) {
+ mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
+ TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getCurrentPage());
+ if (taskView == null) {
+ throw new IllegalStateException("There is no task view to animate to.");
+ }
+ return mRecentsView.createTaskLauncherAnimation(taskView, duration);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
new file mode 100644
index 0000000000000000000000000000000000000000..18b8af4fa764889b450bafbddfcc2b5910f334ee
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -0,0 +1,165 @@
+/*
+ * 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.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.QUICK_SWITCH;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_Y;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
+import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.touch.AbstractStateChangeTouchController;
+import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.quickstep.OverviewInteractionState;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.QuickStepContract;
+
+/**
+ * Handles quick switching to a recent task from the home screen.
+ */
+public class QuickSwitchTouchController extends AbstractStateChangeTouchController {
+
+ private @Nullable TaskView mTaskToLaunch;
+
+ public QuickSwitchTouchController(Launcher launcher) {
+ this(launcher, SwipeDetector.HORIZONTAL);
+ }
+
+ protected QuickSwitchTouchController(Launcher l, SwipeDetector.Direction dir) {
+ super(l, dir);
+ }
+
+ @Override
+ protected boolean canInterceptTouch(MotionEvent ev) {
+ if (mCurrentAnimation != null) {
+ return true;
+ }
+ if (!mLauncher.isInState(LauncherState.NORMAL)) {
+ return false;
+ }
+ if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+ int stateFlags = OverviewInteractionState.INSTANCE.get(mLauncher).getSystemUiStateFlags();
+ if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
+ return NORMAL;
+ }
+ return isDragTowardPositive ? QUICK_SWITCH : NORMAL;
+ }
+
+ @Override
+ public void onDragStart(boolean start) {
+ super.onDragStart(start);
+ mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
+ mTaskToLaunch = mLauncher.getOverviewPanel().getTaskViewAt(0);
+ }
+
+ @Override
+ protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
+ super.onSwipeInteractionCompleted(targetState, logAction);
+ mTaskToLaunch = null;
+ }
+
+ @Override
+ protected float initCurrentAnimation(int animComponents) {
+ AnimatorSetBuilder animatorSetBuilder = new AnimatorSetBuilder();
+ setupInterpolators(animatorSetBuilder);
+ long accuracy = (long) (getShiftRange() * 2);
+ mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState,
+ animatorSetBuilder, accuracy, this::clearState, LauncherStateManager.ANIM_ALL);
+ mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator -> {
+ updateFullscreenProgress((Float) valueAnimator.getAnimatedValue());
+ });
+ return 1 / getShiftRange();
+ }
+
+ private void setupInterpolators(AnimatorSetBuilder animatorSetBuilder) {
+ animatorSetBuilder.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_2);
+ animatorSetBuilder.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_2);
+ if (SysUINavigationMode.getMode(mLauncher) == Mode.NO_BUTTON) {
+ // Overview lives to the left of workspace, so translate down later than over
+ animatorSetBuilder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL_2);
+ animatorSetBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, ACCEL_2);
+ animatorSetBuilder.setInterpolator(ANIM_OVERVIEW_SCALE, ACCEL_2);
+ animatorSetBuilder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, ACCEL_2);
+ animatorSetBuilder.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
+ } else {
+ animatorSetBuilder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, LINEAR);
+ animatorSetBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, LINEAR);
+ }
+ }
+
+ @Override
+ protected void updateProgress(float progress) {
+ super.updateProgress(progress);
+ updateFullscreenProgress(Utilities.boundToRange(progress, 0, 1));
+ }
+
+ private void updateFullscreenProgress(float progress) {
+ if (mTaskToLaunch != null) {
+ mTaskToLaunch.setFullscreenProgress(progress);
+ int sysuiFlags = progress > UPDATE_SYSUI_FLAGS_THRESHOLD
+ ? mTaskToLaunch.getThumbnail().getSysUiStatusNavFlags()
+ : 0;
+ mLauncher.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
+ }
+ }
+
+ @Override
+ protected float getShiftRange() {
+ return mLauncher.getDeviceProfile().widthPx / 2f;
+ }
+
+ @Override
+ protected int getLogContainerTypeForNormalState() {
+ return LauncherLogProto.ContainerType.NAVBAR;
+ }
+
+ @Override
+ protected int getDirectionForLog() {
+ return Utilities.isRtl(mLauncher.getResources()) ? Direction.LEFT : Direction.RIGHT;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
similarity index 88%
rename from quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
rename to quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index cfd41191fa3c496ac3ae9ca7696830bff5084096..8e32bb370e061af403d64cb2c9e9cf90614d028c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -13,10 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.launcher3.uioverrides;
+package com.android.launcher3.uioverrides.touchcontrollers;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -35,7 +38,7 @@ import com.android.launcher3.util.FlingBlockCheck;
import com.android.launcher3.util.PendingAnimation;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.OverviewInteractionState;
+import com.android.quickstep.SysUINavigationMode;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
@@ -48,7 +51,7 @@ public abstract class TaskViewTouchController
private static final String TAG = "OverviewSwipeController";
// Progress after which the transition is assumed to be a success in case user does not fling
- private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
+ public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
protected final T mActivity;
private final SwipeDetector mDetector;
@@ -79,7 +82,7 @@ public abstract class TaskViewTouchController
// If we are already animating from a previous state, we can intercept.
return true;
}
- if (AbstractFloatingView.getTopOpenView(mActivity) != null) {
+ if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) {
return false;
}
return isRecentsInteractive();
@@ -120,8 +123,7 @@ public abstract class TaskViewTouchController
if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
.isEventOverView(view, ev)) {
mTaskBeingDragged = view;
- if (!OverviewInteractionState.getInstance(mActivity)
- .isSwipeUpGestureEnabled()) {
+ if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
// Don't allow swipe down to open if we don't support swipe up
// to enter overview.
directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
@@ -219,7 +221,7 @@ public abstract class TaskViewTouchController
}
@Override
- public boolean onDrag(float displacement, float velocity) {
+ public boolean onDrag(float displacement) {
float totalDisplacement = displacement + mDisplacementShift;
boolean isGoingUp =
totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0;
@@ -229,7 +231,14 @@ public abstract class TaskViewTouchController
} else {
mFlingBlockCheck.onEvent();
}
- mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier);
+ mCurrentAnimation.setPlayFraction(Utilities.boundToRange(
+ totalDisplacement * mProgressMultiplier, 0, 1));
+
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (mRecentsView.getCurrentPage() != 0 || isGoingUp) {
+ mRecentsView.redrawLiveTile(true);
+ }
+ }
return true;
}
@@ -242,7 +251,7 @@ public abstract class TaskViewTouchController
fling = false;
}
float progress = mCurrentAnimation.getProgressFraction();
- float interpolatedProgress = mCurrentAnimation.getInterpolator().getInterpolation(progress);
+ float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
if (fling) {
logAction = Touch.FLING;
boolean goingUp = velocity < 0;
@@ -266,6 +275,16 @@ public abstract class TaskViewTouchController
anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
anim.setDuration(animationDuration);
anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ anim.addUpdateListener(valueAnimator -> {
+ if (mRecentsView.getCurrentPage() != 0 || mCurrentAnimationIsGoingUp) {
+ mRecentsView.redrawLiveTile(true);
+ }
+ });
+ }
+ if (QUICKSTEP_SPRINGS.get()) {
+ mCurrentAnimation.dispatchOnStartWithVelocity(goingToEnd ? 1f : 0f, velocity);
+ }
anim.start();
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
new file mode 100644
index 0000000000000000000000000000000000000000..f1e4041eb2c81dcde7275a0c9f608bc27d3d7def
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
@@ -0,0 +1,44 @@
+/*
+ * 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.uioverrides.touchcontrollers;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.touch.SwipeDetector;
+
+public class TransposedQuickSwitchTouchController extends QuickSwitchTouchController {
+
+ public TransposedQuickSwitchTouchController(Launcher launcher) {
+ super(launcher, SwipeDetector.VERTICAL);
+ }
+
+ @Override
+ protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+ return super.getTargetState(fromState,
+ isDragTowardPositive ^ mLauncher.getDeviceProfile().isSeascape());
+ }
+
+ @Override
+ protected float initCurrentAnimation(int animComponents) {
+ float multiplier = super.initCurrentAnimation(animComponents);
+ return mLauncher.getDeviceProfile().isSeascape() ? multiplier : -multiplier;
+ }
+
+ @Override
+ protected float getShiftRange() {
+ return mLauncher.getDeviceProfile().heightPx / 2f;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..5e77e0adeebe9ff309c6b8267dc6509a6be5f604
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -0,0 +1,177 @@
+/*
+ * 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 com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.View;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.RemoteAnimationProvider;
+import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+
+/**
+ * Provider for the atomic remote window animation from the app to the overview.
+ *
+ * @param activity that contains the overview
+ */
+final class AppToOverviewAnimationProvider implements
+ RemoteAnimationProvider {
+
+ private static final long RECENTS_LAUNCH_DURATION = 250;
+ private static final String TAG = "AppToOverviewAnimationProvider";
+
+ private final ActivityControlHelper mHelper;
+ // The id of the currently running task that is transitioning to overview.
+ private final int mTargetTaskId;
+
+ private T mActivity;
+ private RecentsView mRecentsView;
+
+ AppToOverviewAnimationProvider(ActivityControlHelper helper, int targetTaskId) {
+ mHelper = helper;
+ mTargetTaskId = targetTaskId;
+ }
+
+ /**
+ * Callback for when the activity is ready/initialized.
+ *
+ * @param activity the activity that is ready
+ * @param wasVisible true if it was visible before
+ */
+ boolean onActivityReady(T activity, Boolean wasVisible) {
+ activity.getOverviewPanel().showCurrentTask(mTargetTaskId);
+ AbstractFloatingView.closeAllOpenViews(activity, wasVisible);
+ ActivityControlHelper.AnimationFactory factory =
+ mHelper.prepareRecentsUI(activity, wasVisible,
+ false /* animate activity */, (controller) -> {
+ controller.dispatchOnStart();
+ ValueAnimator anim = controller.getAnimationPlayer()
+ .setDuration(RECENTS_LAUNCH_DURATION);
+ anim.setInterpolator(FAST_OUT_SLOW_IN);
+ anim.start();
+ });
+ factory.onRemoteAnimationReceived(null);
+ factory.createActivityController(RECENTS_LAUNCH_DURATION);
+ mActivity = activity;
+ mRecentsView = mActivity.getOverviewPanel();
+ return false;
+ }
+
+ /**
+ * Create remote window animation from the currently running app to the overview panel.
+ *
+ * @param targetCompats the target apps
+ * @return animation from app to overview
+ */
+ @Override
+ public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) {
+ if (mRecentsView != null) {
+ mRecentsView.setRunningTaskIconScaledDown(true);
+ }
+ AnimatorSet anim = new AnimatorSet();
+ anim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ if (mRecentsView != null) {
+ mRecentsView.animateUpRunningTaskIconScale();
+ }
+ }
+ });
+ if (mActivity == null) {
+ Log.e(TAG, "Animation created, before activity");
+ anim.play(ValueAnimator.ofInt(0, 1).setDuration(RECENTS_LAUNCH_DURATION));
+ return anim;
+ }
+
+ RemoteAnimationTargetSet targetSet =
+ new RemoteAnimationTargetSet(targetCompats, MODE_CLOSING);
+
+ // Use the top closing app to determine the insets for the animation
+ RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mTargetTaskId);
+ if (runningTaskTarget == null) {
+ Log.e(TAG, "No closing app");
+ anim.play(ValueAnimator.ofInt(0, 1).setDuration(RECENTS_LAUNCH_DURATION));
+ return anim;
+ }
+
+ final ClipAnimationHelper clipHelper = new ClipAnimationHelper(mActivity);
+
+ // At this point, the activity is already started and laid-out. Get the home-bounds
+ // relative to the screen using the rootView of the activity.
+ int loc[] = new int[2];
+ View rootView = mActivity.getRootView();
+ rootView.getLocationOnScreen(loc);
+ Rect homeBounds = new Rect(loc[0], loc[1],
+ loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight());
+ clipHelper.updateSource(homeBounds, runningTaskTarget);
+
+ Rect targetRect = new Rect();
+ mHelper.getSwipeUpDestinationAndLength(mActivity.getDeviceProfile(), mActivity, targetRect);
+ clipHelper.updateTargetRect(targetRect);
+ clipHelper.prepareAnimation(mActivity.getDeviceProfile(), false /* isOpening */);
+
+ ClipAnimationHelper.TransformParams params = new ClipAnimationHelper.TransformParams()
+ .setSyncTransactionApplier(new SyncRtSurfaceTransactionApplierCompat(rootView));
+ ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
+ valueAnimator.setDuration(RECENTS_LAUNCH_DURATION);
+ valueAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
+ valueAnimator.addUpdateListener((v) -> {
+ params.setProgress((float) v.getAnimatedValue());
+ clipHelper.applyTransform(targetSet, params);
+ });
+
+ if (targetSet.isAnimatingHome()) {
+ // If we are animating home, fade in the opening targets
+ RemoteAnimationTargetSet openingSet =
+ new RemoteAnimationTargetSet(targetCompats, MODE_OPENING);
+
+ TransactionCompat transaction = new TransactionCompat();
+ valueAnimator.addUpdateListener((v) -> {
+ for (RemoteAnimationTargetCompat app : openingSet.apps) {
+ transaction.setAlpha(app.leash, (float) v.getAnimatedValue());
+ }
+ transaction.apply();
+ });
+ }
+ anim.play(valueAnimator);
+ return anim;
+ }
+
+ /**
+ * Get duration of animation from app to overview.
+ *
+ * @return duration of animation
+ */
+ long getRecentsLaunchDuration() {
+ return RECENTS_LAUNCH_DURATION;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..c43155b73a2101a677678e9d52a6c546e68debd8
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
@@ -0,0 +1,238 @@
+/*
+ * 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 com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.quickstep.fallback.FallbackRecentsView.ZOOM_PROGRESS;
+import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+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.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+
+/**
+ * {@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
+ * to the in-launcher one.
+ */
+public final class FallbackActivityControllerHelper implements
+ ActivityControlHelper {
+
+ public FallbackActivityControllerHelper() { }
+
+ @Override
+ public void onTransitionCancelled(RecentsActivity activity, boolean activityVisible) {
+ // TODO:
+ }
+
+ @Override
+ public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
+ LayoutUtils.calculateFallbackTaskSize(context, dp, outRect);
+ if (dp.isVerticalBarLayout()
+ && SysUINavigationMode.INSTANCE.get(context).getMode() != NO_BUTTON) {
+ Rect targetInsets = dp.getInsets();
+ int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
+ return dp.hotseatBarSizePx + hotseatInset;
+ } else {
+ return dp.heightPx - outRect.bottom;
+ }
+ }
+
+ @Override
+ public void onSwipeUpToRecentsComplete(RecentsActivity activity) {
+ RecentsView recentsView = activity.getOverviewPanel();
+ recentsView.getClearAllButton().setVisibilityAlpha(1);
+ recentsView.setDisallowScrollToClearAll(false);
+ }
+
+ @Override
+ public void onAssistantVisibilityChanged(float visibility) {
+ // TODO:
+ }
+
+ @NonNull
+ @Override
+ public HomeAnimationFactory prepareHomeUI(RecentsActivity activity) {
+ RecentsView recentsView = activity.getOverviewPanel();
+
+ return new HomeAnimationFactory() {
+ @NonNull
+ @Override
+ public RectF getWindowTargetRect() {
+ float centerX = recentsView.getPivotX();
+ float centerY = recentsView.getPivotY();
+ return new RectF(centerX, centerY, centerX, centerY);
+ }
+
+ @NonNull
+ @Override
+ public AnimatorPlaybackController createActivityAnimationToHome() {
+ Animator anim = ObjectAnimator.ofFloat(recentsView, CONTENT_ALPHA, 0);
+ anim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ recentsView.startHome();
+ }
+ });
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.play(anim);
+ long accuracy = 2 * Math.max(recentsView.getWidth(), recentsView.getHeight());
+ return AnimatorPlaybackController.wrap(animatorSet, accuracy);
+ }
+ };
+ }
+
+ @Override
+ public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible,
+ boolean animateActivity, Consumer callback) {
+ if (activityVisible) {
+ return (transitionLength) -> { };
+ }
+
+ FallbackRecentsView rv = activity.getOverviewPanel();
+ rv.setContentAlpha(0);
+ rv.getClearAllButton().setVisibilityAlpha(0);
+ rv.setDisallowScrollToClearAll(true);
+
+ boolean fromState = !animateActivity;
+ rv.setInOverviewState(fromState);
+
+ return new AnimationFactory() {
+
+ boolean isAnimatingToRecents = false;
+
+ @Override
+ public void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) {
+ isAnimatingToRecents = targets != null && targets.isAnimatingHome();
+ if (!isAnimatingToRecents) {
+ rv.setContentAlpha(1);
+ }
+ createActivityController(getSwipeUpDestinationAndLength(
+ activity.getDeviceProfile(), activity, new Rect()));
+ }
+
+ @Override
+ public void createActivityController(long transitionLength) {
+ AnimatorSet animatorSet = new AnimatorSet();
+ if (isAnimatingToRecents) {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(rv, CONTENT_ALPHA, 0, 1);
+ anim.setDuration(transitionLength).setInterpolator(LINEAR);
+ animatorSet.play(anim);
+ }
+
+ ObjectAnimator anim = ObjectAnimator.ofFloat(rv, ZOOM_PROGRESS, 1, 0);
+ anim.setDuration(transitionLength).setInterpolator(LINEAR);
+ animatorSet.play(anim);
+
+ AnimatorPlaybackController controller =
+ AnimatorPlaybackController.wrap(animatorSet, transitionLength);
+
+ // Since we are changing the start position of the UI, reapply the state, at the end
+ controller.setEndAction(() -> {
+ boolean endState = true;
+ rv.setInOverviewState(controller.getInterpolatedProgress() > 0.5 ?
+ endState : fromState);
+ });
+
+ callback.accept(controller);
+ }
+ };
+ }
+
+ @Override
+ public ActivityInitListener createActivityInitListener(
+ BiPredicate onInitListener) {
+ return new RecentsActivityTracker(onInitListener);
+ }
+
+ @Nullable
+ @Override
+ public RecentsActivity getCreatedActivity() {
+ return RecentsActivityTracker.getCurrentActivity();
+ }
+
+ @Nullable
+ @Override
+ public RecentsView getVisibleRecentsView() {
+ RecentsActivity activity = getCreatedActivity();
+ if (activity != null && activity.hasWindowFocus()) {
+ return activity.getOverviewPanel();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+ return false;
+ }
+
+ @Override
+ public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
+ // TODO: Remove this once b/77875376 is fixed
+ return target.sourceContainerBounds;
+ }
+
+ @Override
+ public boolean shouldMinimizeSplitScreen() {
+ // TODO: Remove this once b/77875376 is fixed
+ return false;
+ }
+
+ @Override
+ public int getContainerType() {
+ RecentsActivity activity = getCreatedActivity();
+ boolean visible = activity != null && activity.isStarted() && activity.hasWindowFocus();
+ return visible
+ ? LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER
+ : LauncherLogProto.ContainerType.APP;
+ }
+
+ @Override
+ public boolean isInLiveTileMode() {
+ return false;
+ }
+
+ @Override
+ public void onLaunchTaskFailed(RecentsActivity activity) {
+ // TODO: probably go back to overview instead.
+ activity.getOverviewPanel().startHome();
+ }
+
+ @Override
+ public void onLaunchTaskSuccess(RecentsActivity activity) {
+ activity.onTaskLaunched();
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..b2a71a488289fc07fc57c001377c54e10cffb376
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -0,0 +1,496 @@
+/*
+ * 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.TRANSLATION_Y;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_FADE_ANIM;
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_TRANSLATE_X_ANIM;
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_SHELF_ANIM;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.WindowTransformSwipeHandler.RECENTS_ATTACH_DURATION;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.os.UserHandle;
+import android.util.Log;
+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.LauncherInitListenerEx;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.uioverrides.states.OverviewState;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.views.LauncherRecentsView;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+
+/**
+ * {@link ActivityControlHelper} for the in-launcher recents.
+ */
+public final class LauncherActivityControllerHelper implements ActivityControlHelper {
+
+ private Runnable mAdjustInterpolatorsRunnable;
+
+ @Override
+ public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
+ LayoutUtils.calculateLauncherTaskSize(context, dp, outRect);
+ if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
+ Rect targetInsets = dp.getInsets();
+ int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
+ return dp.hotseatBarSizePx + hotseatInset;
+ } else {
+ return LayoutUtils.getShelfTrackingDistance(context, dp);
+ }
+ }
+
+ @Override
+ public void onTransitionCancelled(Launcher activity, boolean activityVisible) {
+ LauncherState startState = activity.getStateManager().getRestState();
+ activity.getStateManager().goToState(startState, activityVisible);
+ }
+
+ @Override
+ public void onSwipeUpToRecentsComplete(Launcher activity) {
+ // Re apply state in case we did something funky during the transition.
+ activity.getStateManager().reapplyState();
+ DiscoveryBounce.showForOverviewIfNeeded(activity);
+ }
+
+ @Override
+ public void onSwipeUpToHomeComplete(Launcher activity) {
+ // Ensure recents is at the correct position for NORMAL state. For example, when we detach
+ // recents, we assume the first task is invisible, making translation off by one task.
+ activity.getStateManager().reapplyState();
+ }
+
+ @Override
+ public void onAssistantVisibilityChanged(float visibility) {
+ Launcher launcher = getCreatedActivity();
+ if (launcher != null) {
+ launcher.onAssistantVisibilityChanged(visibility);
+ }
+ }
+
+ @NonNull
+ @Override
+ public HomeAnimationFactory prepareHomeUI(Launcher activity) {
+ final DeviceProfile dp = activity.getDeviceProfile();
+ final RecentsView recentsView = activity.getOverviewPanel();
+ final TaskView runningTaskView = recentsView.getRunningTaskView();
+ final View workspaceView;
+ if (runningTaskView != null && runningTaskView.getTask().key.getComponent() != null) {
+ workspaceView = activity.getWorkspace().getFirstMatchForAppClose(
+ runningTaskView.getTask().key.getComponent().getPackageName(),
+ UserHandle.of(runningTaskView.getTask().key.userId));
+ } else {
+ workspaceView = null;
+ }
+ final RectF iconLocation = new RectF();
+ boolean canUseWorkspaceView = workspaceView != null && workspaceView.isAttachedToWindow();
+ FloatingIconView floatingIconView = canUseWorkspaceView
+ ? FloatingIconView.getFloatingIconView(activity, workspaceView,
+ true /* hideOriginal */, iconLocation, false /* isOpening */)
+ : null;
+
+ return new HomeAnimationFactory() {
+ @Nullable
+ @Override
+ public View getFloatingView() {
+ return floatingIconView;
+ }
+
+ @NonNull
+ @Override
+ public RectF getWindowTargetRect() {
+ final int halfIconSize = dp.iconSizePx / 2;
+ final float targetCenterX = dp.availableWidthPx / 2f;
+ final float targetCenterY = dp.availableHeightPx - dp.hotseatBarSizePx;
+
+ if (canUseWorkspaceView) {
+ return iconLocation;
+ } else {
+ // Fallback to animate to center of screen.
+ return new RectF(targetCenterX - halfIconSize, targetCenterY - halfIconSize,
+ targetCenterX + halfIconSize, targetCenterY + halfIconSize);
+ }
+ }
+
+ @NonNull
+ @Override
+ public AnimatorPlaybackController createActivityAnimationToHome() {
+ // Return an empty APC here since we have an non-user controlled animation to home.
+ long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
+ return activity.getStateManager().createAnimationToNewWorkspace(NORMAL, accuracy,
+ 0 /* animComponents */);
+ }
+
+ @Override
+ public void playAtomicAnimation(float velocity) {
+ // Setup workspace with 0 duration to prepare for our staggered animation.
+ LauncherStateManager stateManager = activity.getStateManager();
+ AnimatorSetBuilder builder = new AnimatorSetBuilder();
+ // setRecentsAttachedToAppWindow() will animate recents out.
+ builder.addFlag(AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW);
+ stateManager.createAtomicAnimation(BACKGROUND_APP, NORMAL, builder, ANIM_ALL, 0);
+ builder.build().start();
+
+ // Stop scrolling so that it doesn't interfere with the translation offscreen.
+ recentsView.getScroller().forceFinished(true);
+
+ new StaggeredWorkspaceAnim(activity, workspaceView, velocity).start();
+ }
+ };
+ }
+
+ @Override
+ public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
+ boolean animateActivity, Consumer callback) {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "prepareRecentsUI");
+ }
+ final LauncherState startState = activity.getStateManager().getState();
+
+ LauncherState resetState = startState;
+ if (startState.disableRestore) {
+ resetState = activity.getStateManager().getRestState();
+ }
+ activity.getStateManager().setRestState(resetState);
+
+ 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);
+
+ return new AnimationFactory() {
+ private ShelfAnimState mShelfState;
+ private boolean mIsAttachedToWindow;
+
+ @Override
+ public void createActivityController(long transitionLength) {
+ createActivityControllerInternal(activity, fromState, transitionLength, callback);
+ // Creating the activity controller animation sometimes reapplies the launcher state
+ // (because we set the animation as the current state animation), so we reapply the
+ // attached state here as well to ensure recents is shown/hidden appropriately.
+ if (SysUINavigationMode.getMode(activity) == Mode.NO_BUTTON) {
+ setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
+ }
+ }
+
+ @Override
+ public void adjustActivityControllerInterpolators() {
+ if (mAdjustInterpolatorsRunnable != null) {
+ mAdjustInterpolatorsRunnable.run();
+ }
+ }
+
+ @Override
+ public void onTransitionCancelled() {
+ activity.getStateManager().goToState(startState, false /* animate */);
+ }
+
+ @Override
+ public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
+ long duration) {
+ if (mShelfState == shelfState) {
+ return;
+ }
+ mShelfState = shelfState;
+ activity.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM);
+ if (mShelfState == ShelfAnimState.CANCEL) {
+ return;
+ }
+ float shelfHiddenProgress = BACKGROUND_APP.getVerticalProgress(activity);
+ float shelfOverviewProgress = OVERVIEW.getVerticalProgress(activity);
+ // Peek based on default overview progress so we can see hotseat if we're showing
+ // that instead of predictions in overview.
+ float defaultOverviewProgress = OverviewState.getDefaultVerticalProgress(activity);
+ float shelfPeekingProgress = shelfHiddenProgress
+ - (shelfHiddenProgress - defaultOverviewProgress) * 0.25f;
+ float toProgress = mShelfState == ShelfAnimState.HIDE
+ ? shelfHiddenProgress
+ : mShelfState == ShelfAnimState.PEEK
+ ? shelfPeekingProgress
+ : shelfOverviewProgress;
+ Animator shelfAnim = activity.getStateManager()
+ .createStateElementAnimation(INDEX_SHELF_ANIM, toProgress);
+ shelfAnim.setInterpolator(interpolator);
+ shelfAnim.setDuration(duration).start();
+ }
+
+ @Override
+ public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
+ if (mIsAttachedToWindow == attached && animate) {
+ return;
+ }
+ mIsAttachedToWindow = attached;
+ LauncherRecentsView recentsView = activity.getOverviewPanel();
+ Animator fadeAnim = activity.getStateManager()
+ .createStateElementAnimation(
+ INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+
+ int runningTaskIndex = recentsView.getRunningTaskIndex();
+ if (runningTaskIndex == 0) {
+ // If we are on the first task (we haven't quick switched), translate recents in
+ // from the side. Calculate the start translation based on current scale/scroll.
+ float currScale = recentsView.getScaleX();
+ float scrollOffsetX = recentsView.getScrollOffset();
+
+ float offscreenX = NORMAL.getOverviewScaleAndTranslation(activity).translationX;
+ // The first task is hidden, so offset by its width.
+ int firstTaskWidth = recentsView.getTaskViewAt(0).getWidth();
+ offscreenX -= (firstTaskWidth + recentsView.getPageSpacing()) * currScale;
+ // Offset since scale pushes tasks outwards.
+ offscreenX += firstTaskWidth * (currScale - 1) / 2;
+ offscreenX = Math.max(0, offscreenX);
+ if (recentsView.isRtl()) {
+ offscreenX = -offscreenX;
+ }
+
+ float fromTranslationX = attached ? offscreenX - scrollOffsetX : 0;
+ float toTranslationX = attached ? 0 : offscreenX - scrollOffsetX;
+ activity.getStateManager()
+ .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
+
+ if (!recentsView.isShown() && animate) {
+ recentsView.setTranslationX(fromTranslationX);
+ } else {
+ fromTranslationX = recentsView.getTranslationX();
+ }
+
+ if (!animate) {
+ recentsView.setTranslationX(toTranslationX);
+ } else {
+ activity.getStateManager().createStateElementAnimation(
+ INDEX_RECENTS_TRANSLATE_X_ANIM,
+ fromTranslationX, toTranslationX).start();
+ }
+
+ fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
+ } else {
+ fadeAnim.setInterpolator(ACCEL_DEACCEL);
+ }
+ fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
+ }
+ };
+ }
+
+ private void createActivityControllerInternal(Launcher activity, LauncherState fromState,
+ long transitionLength, Consumer callback) {
+ LauncherState endState = OVERVIEW;
+ if (fromState == endState) {
+ return;
+ }
+
+ AnimatorSet anim = new AnimatorSet();
+ if (!activity.getDeviceProfile().isVerticalBarLayout()
+ && SysUINavigationMode.getMode(activity) != Mode.NO_BUTTON) {
+ // Don't animate the shelf when the mode is NO_BUTTON, because we update it atomically.
+ anim.play(activity.getStateManager().createStateElementAnimation(
+ INDEX_SHELF_ANIM,
+ fromState.getVerticalProgress(activity),
+ endState.getVerticalProgress(activity)));
+ }
+ playScaleDownAnim(anim, activity, fromState, endState);
+
+ anim.setDuration(transitionLength * 2);
+ anim.setInterpolator(LINEAR);
+ AnimatorPlaybackController controller =
+ AnimatorPlaybackController.wrap(anim, transitionLength * 2);
+ activity.getStateManager().setCurrentUserControlledAnimation(controller);
+
+ // Since we are changing the start position of the UI, reapply the state, at the end
+ controller.setEndAction(() -> {
+ activity.getStateManager().goToState(
+ controller.getInterpolatedProgress() > 0.5 ? endState : fromState, false);
+ });
+ callback.accept(controller);
+ }
+
+ /**
+ * Scale down recents from the center task being full screen to being in overview.
+ */
+ private void playScaleDownAnim(AnimatorSet anim, Launcher launcher, LauncherState fromState,
+ LauncherState endState) {
+ RecentsView recentsView = launcher.getOverviewPanel();
+ TaskView v = recentsView.getTaskViewAt(recentsView.getCurrentPage());
+ if (v == null) {
+ return;
+ }
+
+ LauncherState.ScaleAndTranslation fromScaleAndTranslation
+ = fromState.getOverviewScaleAndTranslation(launcher);
+ LauncherState.ScaleAndTranslation endScaleAndTranslation
+ = endState.getOverviewScaleAndTranslation(launcher);
+ float fromTranslationY = fromScaleAndTranslation.translationY;
+ float endTranslationY = endScaleAndTranslation.translationY;
+ float fromFullscreenProgress = fromState.getOverviewFullscreenProgress();
+ float endFullscreenProgress = endState.getOverviewFullscreenProgress();
+
+ Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
+ fromScaleAndTranslation.scale, endScaleAndTranslation.scale);
+ Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y,
+ fromTranslationY, endTranslationY);
+ Animator applyFullscreenProgress = ObjectAnimator.ofFloat(recentsView,
+ RecentsView.FULLSCREEN_PROGRESS, fromFullscreenProgress, endFullscreenProgress);
+ anim.playTogether(scale, translateY, applyFullscreenProgress);
+
+ mAdjustInterpolatorsRunnable = () -> {
+ // Adjust the translateY interpolator to account for the running task's top inset.
+ // When progress <= 1, this is handled by each task view as they set their fullscreen
+ // progress. However, once we go to progress > 1, fullscreen progress stays at 0, so
+ // recents as a whole needs to translate further to keep up with the app window.
+ TaskView runningTaskView = recentsView.getRunningTaskView();
+ if (runningTaskView == null) {
+ runningTaskView = recentsView.getTaskViewAt(recentsView.getCurrentPage());
+ }
+ TimeInterpolator oldInterpolator = translateY.getInterpolator();
+ Rect fallbackInsets = launcher.getDeviceProfile().getInsets();
+ float extraTranslationY = runningTaskView.getThumbnail().getInsets(fallbackInsets).top;
+ float normalizedTranslationY = extraTranslationY / (fromTranslationY - endTranslationY);
+ translateY.setInterpolator(t -> {
+ float newT = oldInterpolator.getInterpolation(t);
+ return newT <= 1f ? newT : newT + normalizedTranslationY * (newT - 1);
+ });
+ };
+ }
+
+ @Override
+ public ActivityInitListener createActivityInitListener(
+ BiPredicate onInitListener) {
+ return new LauncherInitListenerEx(onInitListener);
+ }
+
+ @Nullable
+ @Override
+ public Launcher getCreatedActivity() {
+ LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+ if (app == null) {
+ return null;
+ }
+ return (Launcher) app.getModel().getCallback();
+ }
+
+ @Nullable
+ @UiThread
+ private Launcher getVisibleLauncher() {
+ Launcher launcher = getCreatedActivity();
+ return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
+ launcher : null;
+ }
+
+ @Nullable
+ @Override
+ public RecentsView getVisibleRecentsView() {
+ Launcher launcher = getVisibleLauncher();
+ return launcher != null && launcher.getStateManager().getState().overviewUi
+ ? launcher.getOverviewPanel() : null;
+ }
+
+ @Override
+ public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+ Launcher launcher = getVisibleLauncher();
+ if (launcher == null) {
+ return false;
+ }
+
+ launcher.getUserEventDispatcher().logActionCommand(
+ LauncherLogProto.Action.Command.RECENTS_BUTTON,
+ getContainerType(),
+ LauncherLogProto.ContainerType.TASKSWITCHER);
+ launcher.getStateManager().goToState(OVERVIEW,
+ launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
+ return true;
+ }
+
+ @Override
+ public boolean deferStartingActivity(Region activeNavBarRegion, MotionEvent ev) {
+ return activeNavBarRegion.contains((int) ev.getX(), (int) ev.getY());
+ }
+
+ @Override
+ public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
+ return homeBounds;
+ }
+
+ @Override
+ public boolean shouldMinimizeSplitScreen() {
+ return true;
+ }
+
+ @Override
+ public int getContainerType() {
+ final Launcher launcher = getVisibleLauncher();
+ return launcher != null ? launcher.getStateManager().getState().containerType
+ : LauncherLogProto.ContainerType.APP;
+ }
+
+ @Override
+ public boolean isInLiveTileMode() {
+ Launcher launcher = getCreatedActivity();
+ return launcher != null && launcher.getStateManager().getState() == OVERVIEW &&
+ launcher.isStarted();
+ }
+
+ @Override
+ public void onLaunchTaskFailed(Launcher launcher) {
+ launcher.getStateManager().goToState(OVERVIEW);
+ }
+
+ @Override
+ public void onLaunchTaskSuccess(Launcher launcher) {
+ launcher.getStateManager().moveToRestState();
+ }
+}
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..65f323c7d6195f7542a05cf4967f02155853f489
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Empty activity to start a recents transition
+ */
+public class LockScreenRecentsActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ finish();
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..357c9fc35ebdfc7c4a93adddbde40ffda55450b8
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+package com.android.quickstep;
+
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.launcher3.config.FeatureFlags;
+
+import java.util.StringJoiner;
+import java.util.function.Consumer;
+
+/**
+ * Utility class to help manage multiple callbacks based on different states.
+ */
+public class MultiStateCallback {
+
+ private static final String TAG = "MultiStateCallback";
+ public static final boolean DEBUG_STATES = false;
+
+ private final SparseArray mCallbacks = new SparseArray<>();
+ private final SparseArray> mStateChangeHandlers = new SparseArray<>();
+
+ private final String[] mStateNames;
+
+ public MultiStateCallback(String[] stateNames) {
+ mStateNames = DEBUG_STATES ? stateNames : null;
+ }
+
+ private int mState = 0;
+
+ /**
+ * Adds the provided state flags to the global state and executes any callbacks as a result.
+ */
+ public void setState(int stateFlag) {
+ if (DEBUG_STATES) {
+ Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding "
+ + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState));
+ }
+
+ int oldState = mState;
+ mState = mState | stateFlag;
+
+ int count = mCallbacks.size();
+ for (int i = 0; i < count; i++) {
+ int state = mCallbacks.keyAt(i);
+
+ if ((mState & state) == state) {
+ Runnable callback = mCallbacks.valueAt(i);
+ if (callback != null) {
+ // Set the callback to null, so that it does not run again.
+ mCallbacks.setValueAt(i, null);
+ callback.run();
+ }
+ }
+ }
+ notifyStateChangeHandlers(oldState);
+ }
+
+ /**
+ * Adds the provided state flags to the global state and executes any change handlers
+ * as a result.
+ */
+ public void clearState(int stateFlag) {
+ if (DEBUG_STATES) {
+ Log.d(TAG, "[" + System.identityHashCode(this) + "] Removing "
+ + convertToFlagNames(stateFlag) + " from " + convertToFlagNames(mState));
+ }
+
+ int oldState = mState;
+ mState = mState & ~stateFlag;
+ notifyStateChangeHandlers(oldState);
+ }
+
+ private void notifyStateChangeHandlers(int oldState) {
+ int count = mStateChangeHandlers.size();
+ for (int i = 0; i < count; i++) {
+ int state = mStateChangeHandlers.keyAt(i);
+ boolean wasOn = (state & oldState) == state;
+ boolean isOn = (state & mState) == state;
+
+ if (wasOn != isOn) {
+ mStateChangeHandlers.valueAt(i).accept(isOn);
+ }
+ }
+ }
+
+ /**
+ * Sets the callbacks to be run when the provided states are enabled.
+ * The callback is only run once.
+ */
+ public void addCallback(int stateMask, Runnable callback) {
+ if (FeatureFlags.IS_DOGFOOD_BUILD && mCallbacks.get(stateMask) != null) {
+ throw new IllegalStateException("Multiple callbacks on same state");
+ }
+ mCallbacks.put(stateMask, callback);
+ }
+
+ /**
+ * Sets the handler to be called when the provided states are enabled or disabled.
+ */
+ public void addChangeHandler(int stateMask, Consumer handler) {
+ mStateChangeHandlers.put(stateMask, handler);
+ }
+
+ public int getState() {
+ return mState;
+ }
+
+ public boolean hasStates(int stateMask) {
+ return (mState & stateMask) == stateMask;
+ }
+
+ private String convertToFlagNames(int flags) {
+ StringJoiner joiner = new StringJoiner(", ", "[", " (" + flags + ")]");
+ for (int i = 0; i < mStateNames.length; i++) {
+ if ((flags & (1 << i)) != 0) {
+ joiner.add(mStateNames[i]);
+ }
+ }
+ return joiner.toString();
+ }
+
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..6533c63efaf52d4e559c539e70d101a9ae4bd73c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -0,0 +1,232 @@
+/*
+ * 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.
+ */
+package com.android.quickstep;
+
+import static com.android.systemui.shared.system.ActivityManagerWrapper
+ .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.SystemClock;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.LatencyTrackerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+/**
+ * Helper class to handle various atomic commands for switching between Overview.
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class OverviewCommandHelper {
+
+ private final Context mContext;
+ private final ActivityManagerWrapper mAM;
+ private final RecentsModel mRecentsModel;
+ private final MainThreadExecutor mMainThreadExecutor;
+ private final OverviewComponentObserver mOverviewComponentObserver;
+
+ private long mLastToggleTime;
+
+ public OverviewCommandHelper(Context context, OverviewComponentObserver observer) {
+ mContext = context;
+ mAM = ActivityManagerWrapper.getInstance();
+ mMainThreadExecutor = new MainThreadExecutor();
+ mRecentsModel = RecentsModel.INSTANCE.get(mContext);
+ mOverviewComponentObserver = observer;
+ }
+
+ public void onOverviewToggle() {
+ // If currently screen pinning, do not enter overview
+ if (mAM.isScreenPinningActive()) {
+ return;
+ }
+
+ mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ mMainThreadExecutor.execute(new RecentsActivityCommand<>());
+ }
+
+ public void onOverviewShown(boolean triggeredFromAltTab) {
+ mMainThreadExecutor.execute(new ShowRecentsCommand(triggeredFromAltTab));
+ }
+
+ public void onOverviewHidden() {
+ mMainThreadExecutor.execute(new HideRecentsCommand());
+ }
+
+ public void onTip(int actionType, int viewType) {
+ mMainThreadExecutor.execute(() ->
+ UserEventDispatcher.newInstance(mContext).logActionTip(actionType, viewType));
+ }
+
+ private class ShowRecentsCommand extends RecentsActivityCommand {
+
+ private final boolean mTriggeredFromAltTab;
+
+ ShowRecentsCommand(boolean triggeredFromAltTab) {
+ mTriggeredFromAltTab = triggeredFromAltTab;
+ }
+
+ @Override
+ protected boolean handleCommand(long elapsedTime) {
+ // TODO: Go to the next page if started from alt-tab.
+ return mHelper.getVisibleRecentsView() != null;
+ }
+
+ @Override
+ protected void onTransitionComplete() {
+ if (mTriggeredFromAltTab) {
+ RecentsView rv = (RecentsView) mHelper.getVisibleRecentsView();
+ if (rv == null) {
+ return;
+ }
+
+ // Ensure that recents view has focus so that it receives the followup key inputs
+ TaskView taskView = rv.getNextTaskView();
+ if (taskView == null) {
+ if (rv.getTaskViewCount() > 0) {
+ taskView = (TaskView) rv.getPageAt(0);
+ taskView.requestFocus();
+ } else {
+ rv.requestFocus();
+ }
+ } else {
+ taskView.requestFocus();
+ }
+ }
+ }
+ }
+
+ private class HideRecentsCommand extends RecentsActivityCommand {
+
+ @Override
+ protected boolean handleCommand(long elapsedTime) {
+ RecentsView recents = (RecentsView) mHelper.getVisibleRecentsView();
+ if (recents == null) {
+ return false;
+ }
+ int currentPage = recents.getNextPage();
+ if (currentPage >= 0 && currentPage < recents.getTaskViewCount()) {
+ ((TaskView) recents.getPageAt(currentPage)).launchTask(true);
+ } else {
+ recents.startHome();
+ }
+ return true;
+ }
+ }
+
+ private class RecentsActivityCommand implements Runnable {
+
+ protected final ActivityControlHelper mHelper;
+ private final long mCreateTime;
+ private final AppToOverviewAnimationProvider mAnimationProvider;
+
+ private final long mToggleClickedTime = SystemClock.uptimeMillis();
+ private boolean mUserEventLogged;
+ private ActivityInitListener mListener;
+
+ public RecentsActivityCommand() {
+ mHelper = mOverviewComponentObserver.getActivityControlHelper();
+ mCreateTime = SystemClock.elapsedRealtime();
+ mAnimationProvider =
+ new AppToOverviewAnimationProvider<>(mHelper, RecentsModel.getRunningTaskId());
+
+ // Preload the plan
+ mRecentsModel.getTasks(null);
+ }
+
+ @Override
+ public void run() {
+ long elapsedTime = mCreateTime - mLastToggleTime;
+ mLastToggleTime = mCreateTime;
+
+ if (handleCommand(elapsedTime)) {
+ // Command already handled.
+ return;
+ }
+
+ if (mHelper.switchToRecentsIfVisible(this::onTransitionComplete)) {
+ // If successfully switched, then return
+ return;
+ }
+
+ // Otherwise, start overview.
+ mListener = mHelper.createActivityInitListener(this::onActivityReady);
+ mListener.registerAndStartActivity(mOverviewComponentObserver.getOverviewIntent(),
+ this::createWindowAnimation, mContext, mMainThreadExecutor.getHandler(),
+ mAnimationProvider.getRecentsLaunchDuration());
+ }
+
+ protected boolean handleCommand(long elapsedTime) {
+ // TODO: We need to fix this case with PIP, when an activity first enters PIP, it shows
+ // the menu activity which takes window focus, preventing the right condition from
+ // being run below
+ RecentsView recents = mHelper.getVisibleRecentsView();
+ if (recents != null) {
+ // Launch the next task
+ recents.showNextTask();
+ return true;
+ } else if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) {
+ // The user tried to launch back into overview too quickly, either after
+ // launching an app, or before overview has actually shown, just ignore for now
+ return true;
+ }
+ return false;
+ }
+
+ private boolean onActivityReady(T activity, Boolean wasVisible) {
+ if (!mUserEventLogged) {
+ activity.getUserEventDispatcher().logActionCommand(
+ LauncherLogProto.Action.Command.RECENTS_BUTTON,
+ mHelper.getContainerType(),
+ LauncherLogProto.ContainerType.TASKSWITCHER);
+ mUserEventLogged = true;
+ }
+ return mAnimationProvider.onActivityReady(activity, wasVisible);
+ }
+
+ private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) {
+ if (LatencyTrackerCompat.isEnabled(mContext)) {
+ LatencyTrackerCompat.logToggleRecents(
+ (int) (SystemClock.uptimeMillis() - mToggleClickedTime));
+ }
+
+ mListener.unregister();
+
+ AnimatorSet animatorSet = mAnimationProvider.createWindowAnimation(targetCompats);
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onTransitionComplete();
+ }
+ });
+ return animatorSet;
+ }
+
+ protected void onTransitionComplete() { }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
similarity index 51%
rename from quickstep/src/com/android/quickstep/RecentsActivity.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index 1d7c066ab3cfceddee0245fe04aa513f5978329b..fc29a5663b9d00f2f17bed8fdf428eded39c8b78 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -15,13 +15,12 @@
*/
package com.android.quickstep;
-import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
-import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
-
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION;
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_PRE_DELAY;
-import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator;
+import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.QuickstepAppTransitionManagerImpl
+ .STATUS_BAR_TRANSITION_DURATION;
+import static com.android.launcher3.QuickstepAppTransitionManagerImpl
+ .STATUS_BAR_TRANSITION_PRE_DELAY;
+import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;
import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
@@ -29,26 +28,16 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.app.ActivityOptions;
-import android.content.Intent;
import android.content.res.Configuration;
-import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAnimationRunner;
-import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.badge.BadgeInfo;
-import com.android.launcher3.uioverrides.UiFactory;
-import com.android.launcher3.util.SystemUiController;
-import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.fallback.FallbackRecentsView;
import com.android.quickstep.fallback.RecentsRootView;
@@ -59,46 +48,22 @@ import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
/**
- * A simple activity to show the recently launched tasks
+ * A recents activity that shows the recently launched tasks as swipable task cards.
+ * See {@link com.android.quickstep.views.RecentsView}.
*/
-public class RecentsActivity extends BaseDraggingActivity {
+public final class RecentsActivity extends BaseRecentsActivity {
private Handler mUiHandler = new Handler(Looper.getMainLooper());
private RecentsRootView mRecentsRootView;
private FallbackRecentsView mFallbackRecentsView;
- private Configuration mOldConfig;
-
@Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mOldConfig = new Configuration(getResources().getConfiguration());
- initDeviceProfile();
-
+ protected void initViews() {
setContentView(R.layout.fallback_recents_activity);
mRecentsRootView = findViewById(R.id.drag_layer);
mFallbackRecentsView = findViewById(R.id.overview_panel);
-
mRecentsRootView.setup();
-
- getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
- Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
- RecentsActivityTracker.onRecentsActivityCreate(this);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- int diff = newConfig.diff(mOldConfig);
- if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) {
- onHandleConfigChanged();
- }
- mOldConfig.setTo(newConfig);
- super.onConfigurationChanged(newConfig);
}
@Override
@@ -108,21 +73,15 @@ public class RecentsActivity extends BaseDraggingActivity {
}
public void onRootViewSizeChanged() {
- if (isInMultiWindowModeCompat()) {
+ if (isInMultiWindowMode()) {
onHandleConfigChanged();
}
}
- private void onHandleConfigChanged() {
- mUserEventDispatcher = null;
- initDeviceProfile();
-
- AbstractFloatingView.closeOpenViews(this, true,
- AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
- dispatchDeviceProfileChanged();
-
+ @Override
+ protected void onHandleConfigChanged() {
+ super.onHandleConfigChanged();
mRecentsRootView.setup();
- reapplyUi();
}
@Override
@@ -130,23 +89,12 @@ public class RecentsActivity extends BaseDraggingActivity {
mRecentsRootView.dispatchInsets();
}
- private void initDeviceProfile() {
- // In case we are reusing IDP, create a copy so that we dont conflict with Launcher
- // activity.
- LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
- if (isInMultiWindowModeCompat()) {
- InvariantDeviceProfile idp = appState == null
- ? new InvariantDeviceProfile(this) : appState.getInvariantDeviceProfile();
- DeviceProfile dp = idp.getDeviceProfile(this);
- mDeviceProfile = mRecentsRootView == null ? dp.copy(this)
- : dp.getMultiWindowProfile(this, mRecentsRootView.getLastKnownSize());
- } else {
- // If we are reusing the Invariant device profile, make a copy.
- mDeviceProfile = appState == null
- ? new InvariantDeviceProfile(this).getDeviceProfile(this)
- : appState.getInvariantDeviceProfile().getDeviceProfile(this).copy(this);
- }
- onDeviceProfileInitiated();
+ @Override
+ protected DeviceProfile createDeviceProfile() {
+ DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this);
+ return (mRecentsRootView != null) && isInMultiWindowMode()
+ ? dp.getMultiWindowProfile(this, mRecentsRootView.getLastKnownSize())
+ : super.createDeviceProfile();
}
@Override
@@ -164,11 +112,6 @@ public class RecentsActivity extends BaseDraggingActivity {
return (T) mFallbackRecentsView;
}
- @Override
- public BadgeInfo getBadgeInfoForItem(ItemInfo info) {
- return null;
- }
-
@Override
public ActivityOptions getActivityLaunchOptions(final View v) {
if (!(v instanceof TaskView)) {
@@ -182,7 +125,14 @@ public class RecentsActivity extends BaseDraggingActivity {
@Override
public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
AnimationResult result) {
- result.setAnimation(composeRecentsLaunchAnimator(taskView, targetCompats));
+ AnimatorSet anim = composeRecentsLaunchAnimator(taskView, targetCompats);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mFallbackRecentsView.resetViewUI();
+ }
+ });
+ result.setAnimation(anim);
}
};
return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
@@ -198,7 +148,7 @@ public class RecentsActivity extends BaseDraggingActivity {
RemoteAnimationTargetCompat[] targets) {
AnimatorSet target = new AnimatorSet();
boolean activityClosing = taskIsATargetWithMode(targets, getTaskId(), MODE_CLOSING);
- ClipAnimationHelper helper = new ClipAnimationHelper();
+ ClipAnimationHelper helper = new ClipAnimationHelper(this);
target.play(getRecentsWindowAnimator(taskView, !activityClosing, targets, helper)
.setDuration(RECENTS_LAUNCH_DURATION));
@@ -219,67 +169,16 @@ public class RecentsActivity extends BaseDraggingActivity {
return target;
}
- @Override
- public void invalidateParent(ItemInfo info) { }
-
@Override
protected void onStart() {
// Set the alpha to 1 before calling super, as it may get set back to 0 due to
// onActivityStart callback.
mFallbackRecentsView.setContentAlpha(1);
super.onStart();
- UiFactory.onStart(this);
mFallbackRecentsView.resetTaskVisuals();
}
- @Override
- protected void onStop() {
- super.onStop();
-
- // Workaround for b/78520668, explicitly trim memory once UI is hidden
- onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
- }
-
- @Override
- public void onEnterAnimationComplete() {
- super.onEnterAnimationComplete();
- UiFactory.onEnterAnimationComplete(this);
- }
-
- @Override
- public void onTrimMemory(int level) {
- super.onTrimMemory(level);
- UiFactory.onTrimMemory(this, level);
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- RecentsActivityTracker.onRecentsActivityNewIntent(this);
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- RecentsActivityTracker.onRecentsActivityDestroy(this);
- }
-
- @Override
- public void onBackPressed() {
- // TODO: Launch the task we came from
- startHome();
- }
-
- public void startHome() {
- startActivity(new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
- }
-
- @Override
- public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
- super.dump(prefix, fd, writer, args);
- writer.println(prefix + "Misc:");
- dumpMisc(writer);
+ public void onTaskLaunched() {
+ mFallbackRecentsView.resetTaskVisuals();
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..ddd28a35007f91c1b5f6c49f2bc094600823045c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+package com.android.quickstep;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.FLAG_NO_GESTURES;
+
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.inputconsumers.InputConsumer;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.systemui.shared.system.InputConsumerController;
+
+import java.util.ArrayList;
+import java.util.function.Supplier;
+
+import androidx.annotation.UiThread;
+
+/**
+ * Wrapper around RecentsAnimationController to help with some synchronization
+ */
+public class RecentsAnimationWrapper {
+
+ // A list of callbacks to run when we receive the recents animation target. There are different
+ // than the state callbacks as these run on the current worker thread.
+ private final ArrayList mCallbacks = new ArrayList<>();
+
+ public SwipeAnimationTargetSet targetSet;
+
+ private boolean mWindowThresholdCrossed = false;
+
+ private final InputConsumerController mInputConsumerController;
+ private final Supplier mInputProxySupplier;
+
+ private InputConsumer mInputConsumer;
+ private boolean mTouchInProgress;
+
+ private boolean mFinishPending;
+
+ public RecentsAnimationWrapper(InputConsumerController inputConsumerController,
+ Supplier inputProxySupplier) {
+ mInputConsumerController = inputConsumerController;
+ mInputProxySupplier = inputProxySupplier;
+ }
+
+ public boolean hasTargets() {
+ return targetSet != null && targetSet.hasTargets();
+ }
+
+ @UiThread
+ public synchronized void setController(SwipeAnimationTargetSet targetSet) {
+ Preconditions.assertUIThread();
+ this.targetSet = targetSet;
+
+ if (targetSet == null) {
+ return;
+ }
+ targetSet.setWindowThresholdCrossed(mWindowThresholdCrossed);
+
+ if (!mCallbacks.isEmpty()) {
+ for (Runnable action : new ArrayList<>(mCallbacks)) {
+ action.run();
+ }
+ mCallbacks.clear();
+ }
+ }
+
+ public synchronized void runOnInit(Runnable action) {
+ if (targetSet == null) {
+ mCallbacks.add(action);
+ } else {
+ action.run();
+ }
+ }
+
+ /** See {@link #finish(boolean, Runnable, boolean)} */
+ @UiThread
+ public void finish(boolean toRecents, Runnable onFinishComplete) {
+ finish(toRecents, onFinishComplete, false /* sendUserLeaveHint */);
+ }
+
+ /**
+ * @param onFinishComplete A callback that runs on the main thread after the animation
+ * controller has finished on the background thread.
+ * @param sendUserLeaveHint Determines whether userLeaveHint flag will be set on the pausing
+ * activity. If userLeaveHint is true, the activity will enter into
+ * picture-in-picture mode upon being paused.
+ */
+ @UiThread
+ public void finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint) {
+ Preconditions.assertUIThread();
+ if (!toRecents) {
+ finishAndClear(false, onFinishComplete, sendUserLeaveHint);
+ } else {
+ if (mTouchInProgress) {
+ mFinishPending = true;
+ // Execute the callback
+ if (onFinishComplete != null) {
+ onFinishComplete.run();
+ }
+ } else {
+ finishAndClear(true, onFinishComplete, sendUserLeaveHint);
+ }
+ }
+ }
+
+ private void finishAndClear(boolean toRecents, Runnable onFinishComplete,
+ boolean sendUserLeaveHint) {
+ SwipeAnimationTargetSet controller = targetSet;
+ targetSet = null;
+ if (controller != null) {
+ controller.finishController(toRecents, onFinishComplete, sendUserLeaveHint);
+ }
+ }
+
+ public void enableInputConsumer() {
+ if (targetSet != null) {
+ targetSet.enableInputConsumer();
+ }
+ }
+
+ /**
+ * Indicates that the gesture has crossed the window boundary threshold and system UI can be
+ * update the represent the window behind
+ */
+ public void setWindowThresholdCrossed(boolean windowThresholdCrossed) {
+ if (mWindowThresholdCrossed != windowThresholdCrossed) {
+ mWindowThresholdCrossed = windowThresholdCrossed;
+ if (targetSet != null) {
+ targetSet.setWindowThresholdCrossed(windowThresholdCrossed);
+ }
+ }
+ }
+
+ public void enableInputProxy() {
+ mInputConsumerController.setInputListener(this::onInputConsumerEvent);
+ }
+
+ private boolean onInputConsumerEvent(InputEvent ev) {
+ if (ev instanceof MotionEvent) {
+ onInputConsumerMotionEvent((MotionEvent) ev);
+ } else if (ev instanceof KeyEvent) {
+ if (mInputConsumer == null) {
+ mInputConsumer = mInputProxySupplier.get();
+ }
+ mInputConsumer.onKeyEvent((KeyEvent) ev);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean onInputConsumerMotionEvent(MotionEvent ev) {
+ int action = ev.getAction();
+ if (action == ACTION_DOWN) {
+ mTouchInProgress = true;
+ if (mInputConsumer == null) {
+ mInputConsumer = mInputProxySupplier.get();
+ }
+ } else if (action == ACTION_CANCEL || action == ACTION_UP) {
+ // Finish any pending actions
+ mTouchInProgress = false;
+ if (mFinishPending) {
+ mFinishPending = false;
+ finishAndClear(true /* toRecents */, null, false /* sendUserLeaveHint */);
+ }
+ }
+ if (mInputConsumer != null) {
+ int flags = ev.getEdgeFlags();
+ ev.setEdgeFlags(flags | FLAG_NO_GESTURES);
+ mInputConsumer.onMotionEvent(ev);
+ ev.setEdgeFlags(flags);
+ }
+
+ return true;
+ }
+
+ public void setCancelWithDeferredScreenshot(boolean deferredWithScreenshot) {
+ if (targetSet != null) {
+ targetSet.controller.setCancelWithDeferredScreenshot(deferredWithScreenshot);
+ }
+ }
+
+ public SwipeAnimationTargetSet getController() {
+ return targetSet;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
new file mode 100644
index 0000000000000000000000000000000000000000..c55f656dfd1019393453db8643b804ce49e9eb51
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
@@ -0,0 +1,159 @@
+/*
+ * 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 com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+
+import android.util.Log;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.RecentsAnimationListenerSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import java.io.PrintWriter;
+
+/**
+ * Utility class used to store state information shared across multiple transitions.
+ */
+public class SwipeSharedState implements SwipeAnimationListener {
+
+ private OverviewComponentObserver mOverviewComponentObserver;
+
+ private RecentsAnimationListenerSet mRecentsAnimationListener;
+ private SwipeAnimationTargetSet mLastAnimationTarget;
+
+ private boolean mLastAnimationCancelled = false;
+ private boolean mLastAnimationRunning = false;
+
+ public boolean canGestureBeContinued;
+ public boolean goingToLauncher;
+ public boolean recentsAnimationFinishInterrupted;
+ public int nextRunningTaskId = -1;
+
+ public void setOverviewComponentObserver(OverviewComponentObserver observer) {
+ mOverviewComponentObserver = observer;
+ }
+
+ @Override
+ public final void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
+ mLastAnimationTarget = targetSet;
+
+ mLastAnimationCancelled = false;
+ mLastAnimationRunning = true;
+ }
+
+ private void clearAnimationTarget() {
+ if (mLastAnimationTarget != null) {
+ mLastAnimationTarget.release();
+ mLastAnimationTarget = null;
+ }
+ }
+
+ @Override
+ public final void onRecentsAnimationCanceled() {
+ clearAnimationTarget();
+
+ mLastAnimationCancelled = true;
+ mLastAnimationRunning = false;
+ }
+
+ private void clearListenerState(boolean finishAnimation) {
+ if (mRecentsAnimationListener != null) {
+ mRecentsAnimationListener.removeListener(this);
+ mRecentsAnimationListener.cancelListener();
+ if (mLastAnimationRunning && mLastAnimationTarget != null) {
+ Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(),
+ finishAnimation
+ ? mLastAnimationTarget::finishAnimation
+ : mLastAnimationTarget::cancelAnimation);
+ mLastAnimationTarget = null;
+ }
+ }
+ mRecentsAnimationListener = null;
+ clearAnimationTarget();
+ mLastAnimationCancelled = false;
+ mLastAnimationRunning = false;
+ }
+
+ private void onSwipeAnimationFinished(SwipeAnimationTargetSet targetSet) {
+ if (mLastAnimationTarget == targetSet) {
+ mLastAnimationRunning = false;
+ }
+ }
+
+ public RecentsAnimationListenerSet newRecentsAnimationListenerSet() {
+ Preconditions.assertUIThread();
+
+ if (mLastAnimationRunning) {
+ String msg = "New animation started before completing old animation";
+ if (FeatureFlags.IS_DOGFOOD_BUILD) {
+ throw new IllegalArgumentException(msg);
+ } else {
+ Log.e("SwipeSharedState", msg, new Exception());
+ }
+ }
+
+ clearListenerState(false /* finishAnimation */);
+ boolean shouldMinimiseSplitScreen = mOverviewComponentObserver == null ? false
+ : mOverviewComponentObserver.getActivityControlHelper().shouldMinimizeSplitScreen();
+ mRecentsAnimationListener = new RecentsAnimationListenerSet(
+ shouldMinimiseSplitScreen, this::onSwipeAnimationFinished);
+ mRecentsAnimationListener.addListener(this);
+ return mRecentsAnimationListener;
+ }
+
+ public RecentsAnimationListenerSet getActiveListener() {
+ return mRecentsAnimationListener;
+ }
+
+ public void applyActiveRecentsAnimationState(SwipeAnimationListener listener) {
+ if (mLastAnimationTarget != null) {
+ listener.onRecentsAnimationStart(mLastAnimationTarget);
+ } else if (mLastAnimationCancelled) {
+ listener.onRecentsAnimationCanceled();
+ }
+ }
+
+ /**
+ * Called when a recents animation has finished, but was interrupted before the next task was
+ * launched. The given {@param runningTaskId} should be used as the running task for the
+ * continuing input consumer.
+ */
+ public void setRecentsAnimationFinishInterrupted(int runningTaskId) {
+ recentsAnimationFinishInterrupted = true;
+ nextRunningTaskId = runningTaskId;
+ mLastAnimationTarget = mLastAnimationTarget.cloneWithoutTargets();
+ }
+
+ public void clearAllState(boolean finishAnimation) {
+ clearListenerState(finishAnimation);
+ canGestureBeContinued = false;
+ recentsAnimationFinishInterrupted = false;
+ nextRunningTaskId = -1;
+ goingToLauncher = false;
+ }
+
+ public void dump(String prefix, PrintWriter pw) {
+ pw.println(prefix + "goingToLauncher=" + goingToLauncher);
+ pw.println(prefix + "canGestureBeContinued=" + canGestureBeContinued);
+ pw.println(prefix + "recentsAnimationFinishInterrupted=" + recentsAnimationFinishInterrupted);
+ pw.println(prefix + "nextRunningTaskId=" + nextRunningTaskId);
+ pw.println(prefix + "lastAnimationCancelled=" + mLastAnimationCancelled);
+ pw.println(prefix + "lastAnimationRunning=" + mLastAnimationRunning);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..b90f6c2b17ca4eee60ef8acec78d96d565a69dee
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+package com.android.quickstep;
+
+import android.graphics.Matrix;
+import android.view.View;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Factory class to create and add an overlays on the TaskView
+ */
+public class TaskOverlayFactory implements ResourceBasedOverride {
+
+ /** 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(),
+ new TaskSystemShortcut.SplitScreen(),
+ new TaskSystemShortcut.Pin(),
+ new TaskSystemShortcut.Install(),
+ new TaskSystemShortcut.Freeform()
+ };
+
+ public static final MainThreadInitializedObject INSTANCE =
+ new MainThreadInitializedObject<>(c -> Overrides.getObject(TaskOverlayFactory.class,
+ c, R.string.task_overlay_factory_class));
+
+ public List getEnabledShortcuts(TaskView taskView) {
+ final ArrayList shortcuts = new ArrayList<>();
+ final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
+ for (TaskSystemShortcut menuOption : MENU_OPTIONS) {
+ View.OnClickListener onClickListener =
+ menuOption.getOnClickListener(activity, taskView);
+ if (onClickListener != null) {
+ shortcuts.add(menuOption);
+ }
+ }
+ return shortcuts;
+ }
+
+ public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
+ return new TaskOverlay();
+ }
+
+ public static class TaskOverlay {
+
+ /**
+ * Called when the current task is interactive for the user
+ */
+ public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix) { }
+
+ /**
+ * Called when the overlay is no longer used.
+ */
+ public void reset() { }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
similarity index 71%
rename from quickstep/src/com/android/quickstep/TaskSystemShortcut.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
index e5a2b5e256460a47c49f87cf6d49ba319a093449..213c5d324490df9030aa0ef3bd7253c11d7ee007 100644
--- a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
@@ -16,8 +16,11 @@
package com.android.quickstep;
+import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
+import android.app.Activity;
+import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -33,8 +36,10 @@ import android.view.View;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
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;
@@ -46,6 +51,7 @@ import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
import com.android.systemui.shared.recents.view.RecentsTransition;
+import com.android.systemui.shared.system.ActivityCompat;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;
@@ -63,8 +69,8 @@ public class TaskSystemShortcut extends SystemShortcut
protected T mSystemShortcut;
- protected TaskSystemShortcut(T systemShortcut) {
- super(systemShortcut.iconResId, systemShortcut.labelResId);
+ public TaskSystemShortcut(T systemShortcut) {
+ super(systemShortcut);
mSystemShortcut = systemShortcut;
}
@@ -81,7 +87,7 @@ public class TaskSystemShortcut extends SystemShortcut
public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) {
Task task = view.getTask();
- ShortcutInfo dummyInfo = new ShortcutInfo();
+ WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
dummyInfo.intent = new Intent();
ComponentName component = task.getTopComponent();
dummyInfo.intent.setComponent(component);
@@ -102,26 +108,31 @@ public class TaskSystemShortcut extends SystemShortcut
}
}
- public static class SplitScreen extends TaskSystemShortcut {
+ public static abstract class MultiWindow extends TaskSystemShortcut {
private Handler mHandler;
- public SplitScreen() {
- super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen);
+ public MultiWindow(int iconRes, int textRes) {
+ super(iconRes, textRes);
mHandler = new Handler(Looper.getMainLooper());
}
+ protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
+ protected abstract ActivityOptions makeLaunchOptions(Activity activity);
+ protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
+
@Override
public View.OnClickListener getOnClickListener(
BaseDraggingActivity activity, TaskView taskView) {
- if (activity.getDeviceProfile().isMultiWindowMode) {
- return null;
- }
final Task task = taskView.getTask();
final int taskId = task.key.id;
+ final int displayId = task.key.displayId;
if (!task.isDockable) {
return null;
}
+ if (!isAvailable(activity, displayId)) {
+ return null;
+ }
final RecentsView recentsView = activity.getOverviewPanel();
final TaskThumbnailView thumbnailView = taskView.getThumbnail();
@@ -132,7 +143,7 @@ public class TaskSystemShortcut extends SystemShortcut
public void onLayoutChange(View v, int l, int t, int r, int b,
int oldL, int oldT, int oldR, int oldB) {
taskView.getRootView().removeOnLayoutChangeListener(this);
- recentsView.removeIgnoreResetTask(taskView);
+ recentsView.clearIgnoreResetTask(taskId);
// Start animating in the side pages once launcher has been resized
recentsView.dismissTask(taskView, false, false);
@@ -153,22 +164,13 @@ public class TaskSystemShortcut extends SystemShortcut
dismissTaskMenuView(activity);
- final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition();
- if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
- return;
- }
- boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
- if (ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
- ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft))) {
- ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy();
- try {
- sysUiProxy.onSplitScreenInvoked();
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to notify SysUI of split screen: ", e);
+ ActivityOptions options = makeLaunchOptions(activity);
+ if (options != null
+ && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
+ options)) {
+ if (!onActivityStarted(activity)) {
return;
}
- activity.getUserEventDispatcher().logActionOnControl(TAP,
- LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
// Add a device profile change listener to kick off animating the side tasks
// once we enter multiwindow mode and relayout
activity.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
@@ -177,7 +179,7 @@ public class TaskSystemShortcut extends SystemShortcut
// Hide the task view and wait for the window to be resized
// TODO: Consider animating in launcher and do an in-place start activity
// afterwards
- recentsView.addIgnoreResetTask(taskView);
+ recentsView.setIgnoreResetTask(taskId);
taskView.setAlpha(0f);
};
@@ -205,12 +207,77 @@ public class TaskSystemShortcut extends SystemShortcut
}
};
WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
- future, animStartedListener, mHandler, true /* scaleUp */);
+ future, animStartedListener, mHandler, true /* scaleUp */,
+ v.getDisplay().getDisplayId());
}
});
}
}
+ public static class SplitScreen extends MultiWindow {
+ public SplitScreen() {
+ super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen);
+ }
+
+ @Override
+ protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
+ // Don't show menu-item if already in multi-window and the task is from
+ // the secondary display.
+ // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
+ // implementation is enabled
+ return !activity.getDeviceProfile().isMultiWindowMode
+ && (displayId == -1 || displayId == DEFAULT_DISPLAY);
+ }
+
+ @Override
+ protected ActivityOptions makeLaunchOptions(Activity activity) {
+ final ActivityCompat act = new ActivityCompat(activity);
+ final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
+ act.getDisplayId());
+ if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
+ return null;
+ }
+ boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
+ return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
+ }
+
+ @Override
+ protected boolean onActivityStarted(BaseDraggingActivity activity) {
+ ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
+ try {
+ sysUiProxy.onSplitScreenInvoked();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to notify SysUI of split screen: ", e);
+ return false;
+ }
+ activity.getUserEventDispatcher().logActionOnControl(TAP,
+ LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
+ return true;
+ }
+ }
+
+ public static class Freeform extends MultiWindow {
+ public Freeform() {
+ super(R.drawable.ic_split_screen, R.string.recent_task_option_freeform);
+ }
+
+ @Override
+ protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
+ return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
+ }
+
+ @Override
+ protected ActivityOptions makeLaunchOptions(Activity activity) {
+ return ActivityOptionsCompat.makeFreeformOptions();
+ }
+
+ @Override
+ protected boolean onActivityStarted(BaseDraggingActivity activity) {
+ Launcher.getLauncher(activity).getStateManager().goToState(LauncherState.NORMAL);
+ return true;
+ }
+ }
+
public static class Pin extends TaskSystemShortcut {
private static final String TAG = Pin.class.getSimpleName();
@@ -225,7 +292,7 @@ public class TaskSystemShortcut extends SystemShortcut
@Override
public View.OnClickListener getOnClickListener(
BaseDraggingActivity activity, TaskView taskView) {
- ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy();
+ ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
if (sysUiProxy == null) {
return null;
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..6897c1e7d7e43662503894de141e249ea73686c9
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
@@ -0,0 +1,176 @@
+/*
+ * 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 com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.ComponentName;
+import android.graphics.RectF;
+import android.view.View;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Utilities;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.MultiValueUpdateListener;
+import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+
+/**
+ * Utility class for helpful methods related to {@link TaskView} objects and their tasks.
+ */
+public final class TaskViewUtils {
+
+ private TaskViewUtils() {}
+
+ /**
+ * Try to find a TaskView that corresponds with the component of the launched view.
+ *
+ * If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation.
+ * Otherwise, we will assume we are using a normal app transition, but it's possible that the
+ * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView.
+ */
+ public static TaskView findTaskViewToLaunch(
+ BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) {
+ RecentsView recentsView = activity.getOverviewPanel();
+ if (v instanceof TaskView) {
+ TaskView taskView = (TaskView) v;
+ return recentsView.isTaskViewVisible(taskView) ? taskView : null;
+ }
+
+ // It's possible that the launched view can still be resolved to a visible task view, check
+ // the task id of the opening task and see if we can find a match.
+ if (v.getTag() instanceof ItemInfo) {
+ ItemInfo itemInfo = (ItemInfo) v.getTag();
+ ComponentName componentName = itemInfo.getTargetComponent();
+ int userId = itemInfo.user.getIdentifier();
+ if (componentName != null) {
+ for (int i = 0; i < recentsView.getTaskViewCount(); i++) {
+ TaskView taskView = recentsView.getTaskViewAt(i);
+ if (recentsView.isTaskViewVisible(taskView)) {
+ Task.TaskKey key = taskView.getTask().key;
+ if (componentName.equals(key.getComponent()) && userId == key.userId) {
+ return taskView;
+ }
+ }
+ }
+ }
+ }
+
+ if (targets == null) {
+ return null;
+ }
+ // Resolve the opening task id
+ int openingTaskId = -1;
+ for (RemoteAnimationTargetCompat target : targets) {
+ if (target.mode == MODE_OPENING) {
+ openingTaskId = target.taskId;
+ break;
+ }
+ }
+
+ // If there is no opening task id, fall back to the normal app icon launch animation
+ if (openingTaskId == -1) {
+ return null;
+ }
+
+ // If the opening task id is not currently visible in overview, then fall back to normal app
+ // icon launch animation
+ TaskView taskView = recentsView.getTaskView(openingTaskId);
+ if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
+ return null;
+ }
+ return taskView;
+ }
+
+ /**
+ * @return Animator that controls the window of the opening targets for the recents launch
+ * animation.
+ */
+ public static ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
+ RemoteAnimationTargetCompat[] targets, final ClipAnimationHelper inOutHelper) {
+ SyncRtSurfaceTransactionApplierCompat applier =
+ new SyncRtSurfaceTransactionApplierCompat(v);
+ ClipAnimationHelper.TransformParams params = new ClipAnimationHelper.TransformParams()
+ .setSyncTransactionApplier(applier);
+
+ final RemoteAnimationTargetSet targetSet =
+ new RemoteAnimationTargetSet(targets, MODE_OPENING);
+ targetSet.addDependentTransactionApplier(applier);
+
+ final RecentsView recentsView = v.getRecentsView();
+ final ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+ appAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
+ appAnimator.addUpdateListener(new MultiValueUpdateListener() {
+
+ // Defer fading out the view until after the app window gets faded in
+ final FloatProp mViewAlpha = new FloatProp(1f, 0f, 75, 75, LINEAR);
+ final FloatProp mTaskAlpha = new FloatProp(0f, 1f, 0, 75, LINEAR);
+
+
+ final RectF mThumbnailRect;
+
+ {
+ inOutHelper.setTaskAlphaCallback((t, alpha) -> mTaskAlpha.value);
+
+ inOutHelper.prepareAnimation(
+ BaseActivity.fromContext(v.getContext()).getDeviceProfile(),
+ true /* isOpening */);
+ inOutHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(),
+ targetSet.apps.length == 0 ? null : targetSet.apps[0]);
+
+ mThumbnailRect = new RectF(inOutHelper.getTargetRect());
+ mThumbnailRect.offset(-v.getTranslationX(), -v.getTranslationY());
+ Utilities.scaleRectFAboutCenter(mThumbnailRect, 1 / v.getScaleX());
+ }
+
+ @Override
+ public void onUpdate(float percent) {
+ // TODO: Take into account the current fullscreen progress for animating the insets
+ params.setProgress(1 - percent);
+ RectF taskBounds = inOutHelper.applyTransform(targetSet, params);
+ int taskIndex = recentsView.indexOfChild(v);
+ int centerTaskIndex = recentsView.getCurrentPage();
+ boolean parallaxCenterAndAdjacentTask = taskIndex != centerTaskIndex;
+ if (!skipViewChanges && parallaxCenterAndAdjacentTask) {
+ float scale = taskBounds.width() / mThumbnailRect.width();
+ v.setScaleX(scale);
+ v.setScaleY(scale);
+ v.setTranslationX(taskBounds.centerX() - mThumbnailRect.centerX());
+ v.setTranslationY(taskBounds.centerY() - mThumbnailRect.centerY());
+ v.setAlpha(mViewAlpha.value);
+ }
+ }
+ });
+ appAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ targetSet.release();
+ }
+ });
+ return appAnimator;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
new file mode 100644
index 0000000000000000000000000000000000000000..53da0f92ddf31fc6c7a2b57fe693e571bc3c57b8
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -0,0 +1,742 @@
+/*
+ * 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.
+ */
+package com.android.quickstep;
+
+import static android.view.MotionEvent.ACTION_DOWN;
+
+import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
+import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_HINTS_IN_OVERVIEW;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.config.FeatureFlags.FAKE_LANDSCAPE_UI;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Process;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.Display;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.logging.EventLogArray;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.util.LooperExecutor;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
+import com.android.quickstep.inputconsumers.AssistantTouchConsumer;
+import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
+import com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer;
+import com.android.quickstep.inputconsumers.InputConsumer;
+import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
+import com.android.quickstep.inputconsumers.OverviewInputConsumer;
+import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
+import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
+import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
+import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.InputMonitorCompat;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Wrapper around a list for processing arguments.
+ */
+class ArgList extends LinkedList {
+ public ArgList(List l) {
+ super(l);
+ }
+
+ public String peekArg() {
+ return peekFirst();
+ }
+
+ public String nextArg() {
+ return pollFirst().toLowerCase();
+ }
+
+ public String nextArgExact() {
+ return pollFirst();
+ }
+}
+
+/**
+ * Service connected by system-UI for handling touch interaction.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public class TouchInteractionService extends Service implements
+ NavigationModeChangeListener, DisplayListener {
+
+ public static final MainThreadExecutor MAIN_THREAD_EXECUTOR = new MainThreadExecutor();
+ public static final LooperExecutor BACKGROUND_EXECUTOR =
+ new LooperExecutor(UiThreadHelper.getBackgroundLooper());
+
+ public static final EventLogArray TOUCH_INTERACTION_LOG =
+ new EventLogArray("touch_interaction_log", 40);
+
+ private static final String TAG = "TouchInteractionService";
+
+ private final IBinder mMyBinder = new IOverviewProxy.Stub() {
+
+ public void onActiveNavBarRegionChanges(Region region) {
+ mActiveNavBarRegion = region;
+ }
+
+ public void onInitialize(Bundle bundle) {
+ mISystemUiProxy = ISystemUiProxy.Stub
+ .asInterface(bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
+ MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::initInputMonitor);
+ MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::onSystemUiProxySet);
+ }
+
+ @Override
+ public void onOverviewToggle() {
+ mOverviewCommandHelper.onOverviewToggle();
+ }
+
+ @Override
+ public void onOverviewShown(boolean triggeredFromAltTab) {
+ mOverviewCommandHelper.onOverviewShown(triggeredFromAltTab);
+ }
+
+ @Override
+ public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
+ if (triggeredFromAltTab && !triggeredFromHomeKey) {
+ // onOverviewShownFromAltTab hides the overview and ends at the target app
+ mOverviewCommandHelper.onOverviewHidden();
+ }
+ }
+
+ @Override
+ public void onTip(int actionType, int viewType) {
+ mOverviewCommandHelper.onTip(actionType, viewType);
+ }
+
+ @Override
+ public void onAssistantAvailable(boolean available) {
+ mAssistantAvailable = available;
+ }
+
+ @Override
+ public void onAssistantVisibilityChanged(float visibility) {
+ mLastAssistantVisibility = visibility;
+ MAIN_THREAD_EXECUTOR.execute(
+ TouchInteractionService.this::onAssistantVisibilityChanged);
+ }
+
+ public void onBackAction(boolean completed, int downX, int downY, boolean isButton,
+ boolean gestureSwipeLeft) {
+ if (mOverviewComponentObserver == null) {
+ return;
+ }
+
+ final ActivityControlHelper activityControl =
+ mOverviewComponentObserver.getActivityControlHelper();
+ UserEventDispatcher.newInstance(getBaseContext()).logActionBack(completed, downX, downY,
+ isButton, gestureSwipeLeft, activityControl.getContainerType());
+ }
+
+ public void onSystemUiStateChanged(int stateFlags) {
+ mSystemUiStateFlags = stateFlags;
+ MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::onSystemUiFlagsChanged);
+ }
+
+ /** Deprecated methods **/
+ public void onQuickStep(MotionEvent motionEvent) { }
+
+ public void onQuickScrubEnd() { }
+
+ public void onQuickScrubProgress(float progress) { }
+
+ public void onQuickScrubStart() { }
+
+ public void onPreMotionEvent(int downHitTarget) { }
+
+ public void onMotionEvent(MotionEvent ev) {
+ ev.recycle();
+ }
+
+ public void onBind(ISystemUiProxy iSystemUiProxy) { }
+ };
+
+ private static boolean sConnected = false;
+ private static final SwipeSharedState sSwipeSharedState = new SwipeSharedState();
+
+ public static boolean isConnected() {
+ return sConnected;
+ }
+
+ public static SwipeSharedState getSwipeSharedState() {
+ return sSwipeSharedState;
+ }
+
+ private final InputConsumer mResetGestureInputConsumer =
+ new ResetGestureInputConsumer(sSwipeSharedState);
+
+ private ActivityManagerWrapper mAM;
+ private RecentsModel mRecentsModel;
+ private ISystemUiProxy mISystemUiProxy;
+ private OverviewCommandHelper mOverviewCommandHelper;
+ private OverviewComponentObserver mOverviewComponentObserver;
+ private OverviewInteractionState mOverviewInteractionState;
+ private OverviewCallbacks mOverviewCallbacks;
+ private TaskOverlayFactory mTaskOverlayFactory;
+ private InputConsumerController mInputConsumer;
+ private boolean mAssistantAvailable;
+ private float mLastAssistantVisibility = 0;
+ private @SystemUiStateFlags int mSystemUiStateFlags;
+
+ private boolean mIsUserUnlocked;
+ private BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+ initWhenUserUnlocked();
+ }
+ }
+ };
+
+ private InputConsumer mUncheckedConsumer = InputConsumer.NO_OP;
+ private InputConsumer mConsumer = InputConsumer.NO_OP;
+ private Choreographer mMainChoreographer;
+
+ private Region mActiveNavBarRegion = new Region();
+
+ private InputMonitorCompat mInputMonitorCompat;
+ private InputEventReceiver mInputEventReceiver;
+ private Mode mMode = Mode.THREE_BUTTONS;
+ private int mDefaultDisplayId;
+ private final RectF mSwipeTouchRegion = new RectF();
+ private final RectF mAssistantLeftRegion = new RectF();
+ private final RectF mAssistantRightRegion = new RectF();
+
+ private ComponentName mGestureBlockingActivity;
+
+ private Region mExclusionRegion;
+ private SystemGestureExclusionListenerCompat mExclusionListener;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // Initialize anything here that is needed in direct boot mode.
+ // Everything else should be initialized in initWhenUserUnlocked() below.
+ mMainChoreographer = Choreographer.getInstance();
+ mAM = ActivityManagerWrapper.getInstance();
+
+ if (UserManagerCompat.getInstance(this).isUserUnlocked(Process.myUserHandle())) {
+ initWhenUserUnlocked();
+ } else {
+ mIsUserUnlocked = false;
+ registerReceiver(mUserUnlockedReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
+ }
+
+ mDefaultDisplayId = getSystemService(WindowManager.class).getDefaultDisplay()
+ .getDisplayId();
+ String blockingActivity = getString(R.string.gesture_blocking_activity);
+ mGestureBlockingActivity = TextUtils.isEmpty(blockingActivity) ? null :
+ ComponentName.unflattenFromString(blockingActivity);
+
+ mExclusionListener = new SystemGestureExclusionListenerCompat(mDefaultDisplayId) {
+ @Override
+ @BinderThread
+ public void onExclusionChanged(Region region) {
+ // Assignments are atomic, it should be safe on binder thread
+ mExclusionRegion = region;
+ }
+ };
+
+ onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this));
+ sConnected = true;
+ }
+
+ private void disposeEventHandlers() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitorCompat != null) {
+ mInputMonitorCompat.dispose();
+ mInputMonitorCompat = null;
+ }
+ }
+
+ private void initInputMonitor() {
+ if (!mMode.hasGestures || mISystemUiProxy == null) {
+ return;
+ }
+ disposeEventHandlers();
+
+ try {
+ mInputMonitorCompat = InputMonitorCompat.fromBundle(mISystemUiProxy
+ .monitorGestureInput("swipe-up", mDefaultDisplayId), KEY_EXTRA_INPUT_MONITOR);
+ mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
+ mMainChoreographer, this::onInputEvent);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to create input monitor", e);
+ }
+ initTouchBounds();
+ }
+
+ private int getNavbarSize(String resName) {
+ return ResourceUtils.getNavbarSize(resName, getResources());
+ }
+
+ private void initTouchBounds() {
+ if (!mMode.hasGestures) {
+ return;
+ }
+
+ Display defaultDisplay = getSystemService(WindowManager.class).getDefaultDisplay();
+ Point realSize = new Point();
+ defaultDisplay.getRealSize(realSize);
+ mSwipeTouchRegion.set(0, 0, realSize.x, realSize.y);
+ if (mMode == Mode.NO_BUTTON) {
+ int touchHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+ mSwipeTouchRegion.top = mSwipeTouchRegion.bottom - touchHeight;
+
+ final int assistantWidth = getResources()
+ .getDimensionPixelSize(R.dimen.gestures_assistant_width);
+ final float assistantHeight = Math.max(touchHeight,
+ QuickStepContract.getWindowCornerRadius(getResources()));
+ mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = mSwipeTouchRegion.bottom;
+ mAssistantLeftRegion.top = mAssistantRightRegion.top =
+ mSwipeTouchRegion.bottom - assistantHeight;
+
+ mAssistantLeftRegion.left = 0;
+ mAssistantLeftRegion.right = assistantWidth;
+
+ mAssistantRightRegion.right = mSwipeTouchRegion.right;
+ mAssistantRightRegion.left = mSwipeTouchRegion.right - assistantWidth;
+ } else {
+ mAssistantLeftRegion.setEmpty();
+ mAssistantRightRegion.setEmpty();
+ switch (defaultDisplay.getRotation()) {
+ case Surface.ROTATION_90:
+ mSwipeTouchRegion.left = mSwipeTouchRegion.right
+ - getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+ break;
+ case Surface.ROTATION_270:
+ mSwipeTouchRegion.right = mSwipeTouchRegion.left
+ + getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+ break;
+ default:
+ mSwipeTouchRegion.top = mSwipeTouchRegion.bottom
+ - getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+ }
+ }
+ }
+
+ @Override
+ public void onNavigationModeChanged(Mode newMode) {
+ if (mMode.hasGestures != newMode.hasGestures) {
+ if (newMode.hasGestures) {
+ getSystemService(DisplayManager.class).registerDisplayListener(
+ this, MAIN_THREAD_EXECUTOR.getHandler());
+ } else {
+ getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+ }
+ }
+ mMode = newMode;
+
+ disposeEventHandlers();
+ initInputMonitor();
+
+ if (mMode == Mode.NO_BUTTON) {
+ mExclusionListener.register();
+ } else {
+ mExclusionListener.unregister();
+ }
+ }
+
+ @Override
+ public void onDisplayAdded(int i) { }
+
+ @Override
+ public void onDisplayRemoved(int i) { }
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId != mDefaultDisplayId) {
+ return;
+ }
+
+ initTouchBounds();
+ }
+
+ private void initWhenUserUnlocked() {
+ mRecentsModel = RecentsModel.INSTANCE.get(this);
+ mOverviewComponentObserver = new OverviewComponentObserver(this);
+
+ mOverviewCommandHelper = new OverviewCommandHelper(this, mOverviewComponentObserver);
+ mOverviewInteractionState = OverviewInteractionState.INSTANCE.get(this);
+ mOverviewCallbacks = OverviewCallbacks.get(this);
+ mTaskOverlayFactory = TaskOverlayFactory.INSTANCE.get(this);
+ mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
+ mIsUserUnlocked = true;
+
+ sSwipeSharedState.setOverviewComponentObserver(mOverviewComponentObserver);
+ mInputConsumer.registerInputConsumer();
+ onSystemUiProxySet();
+ onSystemUiFlagsChanged();
+ onAssistantVisibilityChanged();
+
+ // Temporarily disable model preload
+ // new ModelPreload().start(this);
+
+ Utilities.unregisterReceiverSafely(this, mUserUnlockedReceiver);
+ }
+
+ @UiThread
+ private void onSystemUiProxySet() {
+ if (mIsUserUnlocked) {
+ mRecentsModel.setSystemUiProxy(mISystemUiProxy);
+ mOverviewInteractionState.setSystemUiProxy(mISystemUiProxy);
+ }
+ }
+
+ @UiThread
+ private void onSystemUiFlagsChanged() {
+ if (mIsUserUnlocked) {
+ mOverviewInteractionState.setSystemUiStateFlags(mSystemUiStateFlags);
+ mOverviewComponentObserver.onSystemUiStateChanged(mSystemUiStateFlags);
+ }
+ }
+
+ @UiThread
+ private void onAssistantVisibilityChanged() {
+ if (mIsUserUnlocked) {
+ mOverviewComponentObserver.getActivityControlHelper().onAssistantVisibilityChanged(
+ mLastAssistantVisibility);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mIsUserUnlocked) {
+ mInputConsumer.unregisterInputConsumer();
+ mOverviewComponentObserver.onDestroy();
+ }
+ disposeEventHandlers();
+ if (mMode.hasGestures) {
+ getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+ }
+
+ sConnected = false;
+ Utilities.unregisterReceiverSafely(this, mUserUnlockedReceiver);
+ SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
+ mExclusionListener.unregister();
+
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.d(TAG, "Touch service connected");
+ return mMyBinder;
+ }
+
+ private void onInputEvent(InputEvent ev) {
+ if (!(ev instanceof MotionEvent)) {
+ Log.e(TAG, "Unknown event " + ev);
+ return;
+ }
+ MotionEvent event = (MotionEvent) ev;
+ TOUCH_INTERACTION_LOG.addLog("onMotionEvent", event.getActionMasked());
+ if (event.getAction() == ACTION_DOWN) {
+ if (mSwipeTouchRegion.contains(event.getX(), event.getY())) {
+ boolean useSharedState = mConsumer.useSharedSwipeState();
+ mConsumer.onConsumerAboutToBeSwitched();
+ mConsumer = newConsumer(useSharedState, event);
+ TOUCH_INTERACTION_LOG.addLog("setInputConsumer", mConsumer.getType());
+ mUncheckedConsumer = mConsumer;
+ } else if (mIsUserUnlocked && mMode == Mode.NO_BUTTON
+ && canTriggerAssistantAction(event)) {
+ // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we should
+ // not interrupt it. QuickSwitch assumes that interruption can only happen if the
+ // next gesture is also quick switch.
+ mUncheckedConsumer =
+ new AssistantTouchConsumer(this, mISystemUiProxy,
+ mOverviewComponentObserver.getActivityControlHelper(),
+ InputConsumer.NO_OP, mInputMonitorCompat);
+ } else {
+ mUncheckedConsumer = InputConsumer.NO_OP;
+ }
+ }
+ mUncheckedConsumer.onMotionEvent(event);
+ }
+
+ private boolean validSystemUiFlags() {
+ return (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
+ && (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
+ && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
+ || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0);
+ }
+
+ private boolean canTriggerAssistantAction(MotionEvent ev) {
+ return mAssistantAvailable
+ && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
+ && (mAssistantLeftRegion.contains(ev.getX(), ev.getY()) ||
+ mAssistantRightRegion.contains(ev.getX(), ev.getY()))
+ && !ActivityManagerWrapper.getInstance().isLockToAppActive();
+ }
+
+ private InputConsumer newConsumer(boolean useSharedState, MotionEvent event) {
+ boolean isInValidSystemUiState = validSystemUiFlags();
+
+ if (!mIsUserUnlocked) {
+ if (isInValidSystemUiState) {
+ // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
+ // launched while device is locked even after exiting direct boot mode (e.g. camera).
+ return createDeviceLockedInputConsumer(mAM.getRunningTask(0));
+ } else {
+ return mResetGestureInputConsumer;
+ }
+ }
+
+ // When using sharedState, bypass systemState check as this is a followup gesture and the
+ // first gesture started in a valid system state.
+ InputConsumer base = isInValidSystemUiState || useSharedState
+ ? newBaseConsumer(useSharedState, event) : mResetGestureInputConsumer;
+ if (mMode == Mode.NO_BUTTON) {
+ final ActivityControlHelper activityControl =
+ mOverviewComponentObserver.getActivityControlHelper();
+ if (canTriggerAssistantAction(event)) {
+ base = new AssistantTouchConsumer(this, mISystemUiProxy, activityControl, base,
+ mInputMonitorCompat);
+ }
+
+ if ((mSystemUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0) {
+ // Note: we only allow accessibility to wrap this, and it replaces the previous
+ // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
+ base = new ScreenPinnedInputConsumer(this, mISystemUiProxy, activityControl);
+ }
+
+ if ((mSystemUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0) {
+ base = new AccessibilityInputConsumer(this, mISystemUiProxy,
+ (mSystemUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0, base,
+ mInputMonitorCompat, mSwipeTouchRegion);
+ }
+ } else {
+ if ((mSystemUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0) {
+ base = mResetGestureInputConsumer;
+ }
+ }
+ return base;
+ }
+
+ private InputConsumer newBaseConsumer(boolean useSharedState, MotionEvent event) {
+ final RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0);
+ if (!useSharedState) {
+ sSwipeSharedState.clearAllState(false /* finishAnimation */);
+ }
+ if ((mSystemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0) {
+ // This handles apps showing over the lockscreen (e.g. camera)
+ return createDeviceLockedInputConsumer(runningTaskInfo);
+ }
+
+ final ActivityControlHelper activityControl =
+ mOverviewComponentObserver.getActivityControlHelper();
+
+ if (runningTaskInfo == null && !sSwipeSharedState.goingToLauncher
+ && !sSwipeSharedState.recentsAnimationFinishInterrupted) {
+ return mResetGestureInputConsumer;
+ } else if (sSwipeSharedState.recentsAnimationFinishInterrupted) {
+ // If the finish animation was interrupted, then continue using the other activity input
+ // consumer but with the next task as the running task
+ RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
+ info.id = sSwipeSharedState.nextRunningTaskId;
+ return createOtherActivityInputConsumer(event, info);
+ } else if (sSwipeSharedState.goingToLauncher || activityControl.isResumed()) {
+ return createOverviewInputConsumer(event);
+ } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityControl.isInLiveTileMode()) {
+ return createOverviewInputConsumer(event);
+ } else if (mGestureBlockingActivity != null && runningTaskInfo != null
+ && mGestureBlockingActivity.equals(runningTaskInfo.topActivity)) {
+ return mResetGestureInputConsumer;
+ } else if (mMode == Mode.NO_BUTTON && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+ return new FallbackNoButtonInputConsumer(this, activityControl,
+ mInputMonitorCompat, sSwipeSharedState, mSwipeTouchRegion,
+ mOverviewComponentObserver, disableHorizontalSwipe(event), runningTaskInfo);
+ } else {
+ return createOtherActivityInputConsumer(event, runningTaskInfo);
+ }
+ }
+
+ private boolean disableHorizontalSwipe(MotionEvent event) {
+ // mExclusionRegion can change on binder thread, use a local instance here.
+ Region exclusionRegion = mExclusionRegion;
+ return mMode == Mode.NO_BUTTON && exclusionRegion != null
+ && exclusionRegion.contains((int) event.getX(), (int) event.getY());
+ }
+
+ private OtherActivityInputConsumer createOtherActivityInputConsumer(MotionEvent event,
+ RunningTaskInfo runningTaskInfo) {
+ final ActivityControlHelper activityControl =
+ mOverviewComponentObserver.getActivityControlHelper();
+ boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event);
+
+ return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel,
+ mOverviewComponentObserver.getOverviewIntent(), activityControl,
+ shouldDefer, mOverviewCallbacks, mInputConsumer, this::onConsumerInactive,
+ sSwipeSharedState, mInputMonitorCompat, mSwipeTouchRegion,
+ disableHorizontalSwipe(event));
+ }
+
+ private InputConsumer createDeviceLockedInputConsumer(RunningTaskInfo taskInfo) {
+ if (mMode == Mode.NO_BUTTON && taskInfo != null) {
+ return new DeviceLockedInputConsumer(this, sSwipeSharedState, mInputMonitorCompat,
+ mSwipeTouchRegion, taskInfo.taskId);
+ } else {
+ return mResetGestureInputConsumer;
+ }
+ }
+
+ public InputConsumer createOverviewInputConsumer(MotionEvent event) {
+ final ActivityControlHelper activityControl =
+ mOverviewComponentObserver.getActivityControlHelper();
+ BaseDraggingActivity activity = activityControl.getCreatedActivity();
+ if (activity == null) {
+ return mResetGestureInputConsumer;
+ }
+
+ if (activity.getRootView().hasWindowFocus() || sSwipeSharedState.goingToLauncher) {
+ return new OverviewInputConsumer(activity, mInputMonitorCompat,
+ false /* startingInActivityBounds */);
+ } else {
+ return new OverviewWithoutFocusInputConsumer(this, mInputMonitorCompat,
+ disableHorizontalSwipe(event));
+ }
+ }
+
+ /**
+ * To be called by the consumer when it's no longer active.
+ */
+ private void onConsumerInactive(InputConsumer caller) {
+ if (mConsumer == caller) {
+ mConsumer = mResetGestureInputConsumer;
+ mUncheckedConsumer = mConsumer;
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] rawArgs) {
+ if (rawArgs.length > 0 && Utilities.IS_DEBUG_DEVICE) {
+ ArgList args = new ArgList(Arrays.asList(rawArgs));
+ switch (args.nextArg()) {
+ case "cmd":
+ if (args.peekArg() == null) {
+ printAvailableCommands(pw);
+ } else {
+ onCommand(pw, args);
+ }
+ break;
+ }
+ } else {
+ // Dump everything
+ pw.println("TouchState:");
+ pw.println(" navMode=" + mMode);
+ pw.println(" validSystemUiFlags=" + validSystemUiFlags());
+ pw.println(" systemUiFlags=" + mSystemUiStateFlags);
+ pw.println(" systemUiFlagsDesc="
+ + QuickStepContract.getSystemUiStateString(mSystemUiStateFlags));
+ pw.println(" assistantAvailable=" + mAssistantAvailable);
+ pw.println(" assistantDisabled="
+ + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
+ pw.println(" resumed="
+ + mOverviewComponentObserver.getActivityControlHelper().isResumed());
+ pw.println(" useSharedState=" + mConsumer.useSharedSwipeState());
+ if (mConsumer.useSharedSwipeState()) {
+ sSwipeSharedState.dump(" ", pw);
+ }
+ pw.println(" mConsumer=" + mConsumer.getName());
+ pw.println("FeatureFlags:");
+ pw.println(" APPLY_CONFIG_AT_RUNTIME=" + APPLY_CONFIG_AT_RUNTIME.get());
+ pw.println(" QUICKSTEP_SPRINGS=" + QUICKSTEP_SPRINGS.get());
+ pw.println(" ADAPTIVE_ICON_WINDOW_ANIM=" + ADAPTIVE_ICON_WINDOW_ANIM.get());
+ pw.println(" ENABLE_QUICKSTEP_LIVE_TILE=" + ENABLE_QUICKSTEP_LIVE_TILE.get());
+ pw.println(" ENABLE_HINTS_IN_OVERVIEW=" + ENABLE_HINTS_IN_OVERVIEW.get());
+ pw.println(" FAKE_LANDSCAPE_UI=" + FAKE_LANDSCAPE_UI.get());
+ TOUCH_INTERACTION_LOG.dump("", pw);
+
+ }
+ }
+
+ private void printAvailableCommands(PrintWriter pw) {
+ pw.println("Available commands:");
+ pw.println(" clear-touch-log: Clears the touch interaction log");
+ }
+
+ private void onCommand(PrintWriter pw, ArgList args) {
+ switch (args.nextArg()) {
+ case "clear-touch-log":
+ TOUCH_INTERACTION_LOG.clear();
+ break;
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..476bb8f9358004613780a3e7caf3f9c143001897
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -0,0 +1,1481 @@
+/*
+ * 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.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
+import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
+import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+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.util.RaceConditionTracker.ENTER;
+import static com.android.launcher3.util.RaceConditionTracker.EXIT;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.HIDE;
+import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.PEEK;
+import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.HOME;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.NEW_TASK;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.RECENTS;
+import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnApplyWindowInsetsListener;
+import android.view.ViewTreeObserver.OnDrawListener;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.util.RaceConditionTracker;
+import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+import com.android.quickstep.ActivityControlHelper.AnimationFactory;
+import com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState;
+import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.inputconsumers.InputConsumer;
+import com.android.quickstep.inputconsumers.OverviewInputConsumer;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import com.android.quickstep.views.LiveTileOverlay;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.LatencyTrackerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+import com.android.systemui.shared.system.WindowCallbacksCompat;
+
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+@TargetApi(Build.VERSION_CODES.O)
+public class WindowTransformSwipeHandler
+ implements SwipeAnimationListener, OnApplyWindowInsetsListener {
+ private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
+
+ private static final Rect TEMP_RECT = new Rect();
+
+ private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
+
+ private static int getFlagForIndex(int index, String name) {
+ if (DEBUG_STATES) {
+ STATE_NAMES[index] = name;
+ }
+ return 1 << index;
+ }
+
+ // Launcher UI related states
+ private static final int STATE_LAUNCHER_PRESENT = getFlagForIndex(0, "STATE_LAUNCHER_PRESENT");
+ private static final int STATE_LAUNCHER_STARTED = getFlagForIndex(1, "STATE_LAUNCHER_STARTED");
+ private static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN");
+
+ // Internal initialization states
+ private static final int STATE_APP_CONTROLLER_RECEIVED =
+ getFlagForIndex(3, "STATE_APP_CONTROLLER_RECEIVED");
+
+ // Interaction finish states
+ private static final int STATE_SCALED_CONTROLLER_HOME =
+ getFlagForIndex(4, "STATE_SCALED_CONTROLLER_HOME");
+ private static final int STATE_SCALED_CONTROLLER_RECENTS =
+ getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS");
+
+ private static final int STATE_HANDLER_INVALIDATED =
+ getFlagForIndex(6, "STATE_HANDLER_INVALIDATED");
+ private static final int STATE_GESTURE_STARTED =
+ getFlagForIndex(7, "STATE_GESTURE_STARTED");
+ private static final int STATE_GESTURE_CANCELLED =
+ getFlagForIndex(8, "STATE_GESTURE_CANCELLED");
+ private static final int STATE_GESTURE_COMPLETED =
+ getFlagForIndex(9, "STATE_GESTURE_COMPLETED");
+
+ private static final int STATE_CAPTURE_SCREENSHOT =
+ getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT");
+ private static final int STATE_SCREENSHOT_CAPTURED =
+ getFlagForIndex(11, "STATE_SCREENSHOT_CAPTURED");
+ private static final int STATE_SCREENSHOT_VIEW_SHOWN =
+ getFlagForIndex(12, "STATE_SCREENSHOT_VIEW_SHOWN");
+
+ private static final int STATE_RESUME_LAST_TASK =
+ getFlagForIndex(13, "STATE_RESUME_LAST_TASK");
+ private static final int STATE_START_NEW_TASK =
+ getFlagForIndex(14, "STATE_START_NEW_TASK");
+ private static final int STATE_CURRENT_TASK_FINISHED =
+ getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED");
+
+ private static final int LAUNCHER_UI_STATES =
+ STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
+
+ public enum GestureEndTarget {
+ HOME(1, STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT, true, false,
+ ContainerType.WORKSPACE, false),
+
+ RECENTS(1, STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
+ | STATE_SCREENSHOT_VIEW_SHOWN, true, false, ContainerType.TASKSWITCHER, true),
+
+ NEW_TASK(0, STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT, false, true,
+ ContainerType.APP, true),
+
+ LAST_TASK(0, STATE_RESUME_LAST_TASK, false, true, ContainerType.APP, false);
+
+ GestureEndTarget(float endShift, int endState, boolean isLauncher, boolean canBeContinued,
+ int containerType, boolean recentsAttachedToAppWindow) {
+ this.endShift = endShift;
+ this.endState = endState;
+ this.isLauncher = isLauncher;
+ this.canBeContinued = canBeContinued;
+ this.containerType = containerType;
+ this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
+ }
+
+ /** 0 is app, 1 is overview */
+ public final float endShift;
+ /** The state to apply when we reach this final target */
+ public final int endState;
+ /** Whether the target is in the launcher activity */
+ public final boolean isLauncher;
+ /** Whether the user can start a new gesture while this one is finishing */
+ public final boolean canBeContinued;
+ /** Used to log where the user ended up after the gesture ends */
+ public final int containerType;
+ /** Whether RecentsView should be attached to the window as we animate to this target */
+ public final boolean recentsAttachedToAppWindow;
+ }
+
+ public static final long MAX_SWIPE_DURATION = 350;
+ public static final long MIN_SWIPE_DURATION = 80;
+ public static final long MIN_OVERSHOOT_DURATION = 120;
+
+ public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
+ private static final float SWIPE_DURATION_MULTIPLIER =
+ Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
+ private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
+
+ private static final long SHELF_ANIM_DURATION = 240;
+ public static final long RECENTS_ATTACH_DURATION = 300;
+
+ // Start resisting when swiping past this factor of mTransitionDragLength.
+ private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
+ // This is how far down we can scale down, where 0f is full screen and 1f is recents.
+ private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
+ private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
+
+ /**
+ * Used as the page index for logging when we return to the last task at the end of the gesture.
+ */
+ private static final int LOG_NO_OP_PAGE_INDEX = -1;
+
+ private final ClipAnimationHelper mClipAnimationHelper;
+ private final ClipAnimationHelper.TransformParams mTransformParams;
+
+ private Runnable mGestureEndCallback;
+ private GestureEndTarget mGestureEndTarget;
+ // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
+ private RunningWindowAnim mRunningWindowAnim;
+ private boolean mIsShelfPeeking;
+ private DeviceProfile mDp;
+ // The distance needed to drag to reach the task size in recents.
+ private int mTransitionDragLength;
+ // How much further we can drag past recents, as a factor of mTransitionDragLength.
+ private float mDragLengthFactor = 1;
+
+ // Shift in the range of [0, 1].
+ // 0 => preview snapShot is completely visible, and hotseat is completely translated down
+ // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
+ // visible.
+ private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+ private boolean mContinuingLastGesture;
+ // To avoid UI jump when gesture is started, we offset the animation by the threshold.
+ private float mShiftAtGestureStart = 0;
+
+ private final Handler mMainThreadHandler = MAIN_THREAD_EXECUTOR.getHandler();
+
+ private final Context mContext;
+ private final ActivityControlHelper mActivityControlHelper;
+ private final ActivityInitListener mActivityInitListener;
+
+ private final SysUINavigationMode.Mode mMode;
+
+ private final int mRunningTaskId;
+ private ThumbnailData mTaskSnapshot;
+
+ private MultiStateCallback mStateCallback;
+ // Used to control launcher components throughout the swipe gesture.
+ private AnimatorPlaybackController mLauncherTransitionController;
+ private boolean mHasLauncherTransitionControllerStarted;
+
+ private T mActivity;
+ private RecentsView mRecentsView;
+ private AnimationFactory mAnimationFactory = (t) -> { };
+ private LiveTileOverlay mLiveTileOverlay = new LiveTileOverlay();
+
+ private boolean mCanceled;
+ private boolean mWasLauncherAlreadyVisible;
+ private int mFinishingRecentsAnimationForNewTaskId = -1;
+
+ private boolean mPassedOverviewThreshold;
+ private boolean mGestureStarted;
+ private int mLogAction = Touch.SWIPE;
+ private int mLogDirection = Direction.UP;
+ private PointF mDownPos;
+ private boolean mIsLikelyToStartNewTask;
+
+ private final RecentsAnimationWrapper mRecentsAnimationWrapper;
+
+ private final long mTouchTimeMs;
+ private long mLauncherFrameDrawnTime;
+
+ public WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context,
+ long touchTimeMs, ActivityControlHelper controller, boolean continuingLastGesture,
+ InputConsumerController inputConsumer) {
+ mContext = context;
+ mRunningTaskId = runningTaskInfo.id;
+ mTouchTimeMs = touchTimeMs;
+ mActivityControlHelper = controller;
+ mActivityInitListener = mActivityControlHelper
+ .createActivityInitListener(this::onActivityInit);
+ mContinuingLastGesture = continuingLastGesture;
+ mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer,
+ this::createNewInputProxyHandler);
+ mClipAnimationHelper = new ClipAnimationHelper(context);
+ mTransformParams = new ClipAnimationHelper.TransformParams();
+
+ mMode = SysUINavigationMode.getMode(context);
+ initStateCallbacks();
+
+ DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
+ initTransitionEndpoints(dp);
+ }
+
+ private void initStateCallbacks() {
+ mStateCallback = new MultiStateCallback(STATE_NAMES);
+
+ mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
+ this::onLauncherPresentAndGestureStarted);
+
+ mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
+ this::initializeLauncherAnimationController);
+
+ mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
+ this::launcherFrameDrawn);
+
+ mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
+ | STATE_GESTURE_CANCELLED,
+ this::resetStateForAnimationCancel);
+
+ mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
+ this::sendRemoteAnimationsToAnimationFactory);
+
+ mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
+ this::resumeLastTask);
+ mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
+ this::startNewTask);
+
+ mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
+ | STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT,
+ this::switchToScreenshot);
+
+ mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+ | STATE_SCALED_CONTROLLER_RECENTS,
+ this::finishCurrentTransitionToRecents);
+
+ mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+ | STATE_SCALED_CONTROLLER_HOME,
+ this::finishCurrentTransitionToHome);
+ mStateCallback.addCallback(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
+ this::reset);
+
+ mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
+ | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS
+ | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED
+ | STATE_GESTURE_STARTED,
+ this::setupLauncherUiAfterSwipeUpToRecentsAnimation);
+
+ mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
+ mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+ this::invalidateHandlerWithLauncher);
+ mStateCallback.addCallback(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
+ this::notifyTransitionCancelled);
+
+ mStateCallback.addCallback(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
+ mRecentsAnimationWrapper::enableInputConsumer);
+
+ if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mStateCallback.addChangeHandler(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
+ | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
+ (b) -> mRecentsView.setRunningTaskHidden(!b));
+ }
+ }
+
+ private void setStateOnUiThread(int stateFlag) {
+ if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+ mStateCallback.setState(stateFlag);
+ } else {
+ postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag));
+ }
+ }
+
+ private void initTransitionEndpoints(DeviceProfile dp) {
+ mDp = dp;
+
+ Rect tempRect = new Rect();
+ mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
+ dp, mContext, tempRect);
+ mClipAnimationHelper.updateTargetRect(tempRect);
+ if (mMode == Mode.NO_BUTTON) {
+ // We can drag all the way to the top of the screen.
+ mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
+ }
+ }
+
+ private long getFadeInDuration() {
+ if (mCurrentShift.getCurrentAnimation() != null) {
+ ObjectAnimator anim = mCurrentShift.getCurrentAnimation();
+ long theirDuration = anim.getDuration() - anim.getCurrentPlayTime();
+
+ // TODO: Find a better heuristic
+ return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION));
+ } else {
+ return MAX_SWIPE_DURATION;
+ }
+ }
+
+ public void initWhenReady() {
+ mActivityInitListener.register();
+ }
+
+ private boolean onActivityInit(final T activity, Boolean alreadyOnHome) {
+ if (mActivity == activity) {
+ return true;
+ }
+ if (mActivity != null) {
+ // The launcher may have been recreated as a result of device rotation.
+ int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
+ initStateCallbacks();
+ mStateCallback.setState(oldState);
+ }
+ mWasLauncherAlreadyVisible = alreadyOnHome;
+ mActivity = activity;
+ // Override the visibility of the activity until the gesture actually starts and we swipe
+ // up, or until we transition home and the home animation is composed
+ if (alreadyOnHome) {
+ mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+ } else {
+ mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+ }
+
+ mRecentsView = activity.getOverviewPanel();
+ SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> {
+ mTransformParams.setSyncTransactionApplier(applier);
+ mRecentsAnimationWrapper.runOnInit(() ->
+ mRecentsAnimationWrapper.targetSet.addDependentTransactionApplier(applier));
+ });
+
+ mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+ if (mGestureEndTarget != HOME) {
+ updateFinalShift();
+ }
+ });
+ mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper);
+ mRecentsView.setClipAnimationHelper(mClipAnimationHelper);
+ mRecentsView.setLiveTileOverlay(mLiveTileOverlay);
+ mActivity.getRootView().getOverlay().add(mLiveTileOverlay);
+
+ mStateCallback.setState(STATE_LAUNCHER_PRESENT);
+ if (alreadyOnHome) {
+ onLauncherStart(activity);
+ } else {
+ activity.setOnStartCallback(this::onLauncherStart);
+ }
+
+ setupRecentsViewUi();
+ return true;
+ }
+
+ private void onLauncherStart(final T activity) {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart");
+ }
+ if (mActivity != activity) {
+ return;
+ }
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 1");
+ }
+ if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
+ return;
+ }
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 2");
+ }
+
+ // 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) {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 3");
+ }
+ Runnable initAnimFactory = () -> {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 4");
+ }
+ mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
+ mWasLauncherAlreadyVisible, true,
+ this::onAnimatorPlaybackControllerCreated);
+ maybeUpdateRecentsAttachedState(false /* animate */);
+ };
+ 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.
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 5");
+ }
+ mStateCallback.addCallback(STATE_GESTURE_STARTED, initAnimFactory);
+ } else {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 6");
+ }
+ initAnimFactory.run();
+ }
+ }
+ AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible,
+ AbstractFloatingView.TYPE_LISTENER);
+
+ if (mWasLauncherAlreadyVisible) {
+ mStateCallback.setState(STATE_LAUNCHER_DRAWN);
+ } else {
+ TraceHelper.beginSection("WTS-init");
+ View dragLayer = activity.getDragLayer();
+ dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
+
+ @Override
+ public void onDraw() {
+ TraceHelper.endSection("WTS-init", "Launcher frame is drawn");
+ dragLayer.post(() ->
+ dragLayer.getViewTreeObserver().removeOnDrawListener(this));
+ if (activity != mActivity) {
+ return;
+ }
+
+ mStateCallback.setState(STATE_LAUNCHER_DRAWN);
+ }
+ });
+ }
+
+ activity.getRootView().setOnApplyWindowInsetsListener(this);
+ mStateCallback.setState(STATE_LAUNCHER_STARTED);
+ }
+
+ private void onLauncherPresentAndGestureStarted() {
+ // Re-setup the recents UI when gesture starts, as the state could have been changed during
+ // that time by a previous window transition.
+ setupRecentsViewUi();
+
+ notifyGestureStartedAsync();
+ }
+
+ private void setupRecentsViewUi() {
+ if (mContinuingLastGesture) {
+ updateSysUiFlags(mCurrentShift.value);
+ return;
+ }
+ mRecentsView.onGestureAnimationStart(mRunningTaskId);
+ }
+
+ private void launcherFrameDrawn() {
+ mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
+ }
+
+ private void sendRemoteAnimationsToAnimationFactory() {
+ mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet);
+ }
+
+ private void initializeLauncherAnimationController() {
+ buildAnimationController();
+
+ if (LatencyTrackerCompat.isEnabled(mContext)) {
+ LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs));
+ }
+
+ // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
+ // high-res thumbnail loader here once we are sure that we will end up in an overview state
+ RecentsModel.INSTANCE.get(mContext).getThumbnailCache()
+ .getHighResLoadingState().setVisible(true);
+ }
+
+ private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
+ float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 +
+ mContext.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+ float interpolation = Math.min(1, offsetX / distanceToReachEdge);
+ return TaskView.getCurveScaleForInterpolation(interpolation);
+ }
+
+ public Consumer getRecentsViewDispatcher(RotationMode rotationMode) {
+ return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null;
+ }
+
+ @UiThread
+ public void updateDisplacement(float displacement) {
+ // We are moving in the negative x/y direction
+ displacement = -displacement;
+ if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
+ mCurrentShift.updateValue(mDragLengthFactor);
+ } else {
+ float translation = Math.max(displacement, 0);
+ float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
+ if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) {
+ float pullbackProgress = Utilities.getProgress(shift,
+ DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor);
+ pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
+ shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress
+ * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK);
+ }
+ mCurrentShift.updateValue(shift);
+ }
+ }
+
+ public void onMotionPauseChanged(boolean isPaused) {
+ setShelfState(isPaused ? PEEK : HIDE, OVERSHOOT_1_2, SHELF_ANIM_DURATION);
+ }
+
+ public void maybeUpdateRecentsAttachedState() {
+ maybeUpdateRecentsAttachedState(true /* animate */);
+ }
+
+ /**
+ * Determines whether to show or hide RecentsView. The window is always
+ * synchronized with its corresponding TaskView in RecentsView, so if
+ * RecentsView is shown, it will appear to be attached to the window.
+ *
+ * Note this method has no effect unless the navigation mode is NO_BUTTON.
+ */
+ private void maybeUpdateRecentsAttachedState(boolean animate) {
+ if (mMode != Mode.NO_BUTTON || mRecentsView == null) {
+ return;
+ }
+ RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationWrapper.targetSet == null
+ ? null
+ : mRecentsAnimationWrapper.targetSet.findTask(mRunningTaskId);
+ final boolean recentsAttachedToAppWindow;
+ int runningTaskIndex = mRecentsView.getRunningTaskIndex();
+ if (mGestureEndTarget != null) {
+ recentsAttachedToAppWindow = mGestureEndTarget.recentsAttachedToAppWindow;
+ } else if (mContinuingLastGesture
+ && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
+ recentsAttachedToAppWindow = true;
+ animate = false;
+ } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
+ // The window is going away so make sure recents is always visible in this case.
+ recentsAttachedToAppWindow = true;
+ animate = false;
+ } else {
+ recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
+ if (animate) {
+ // Only animate if an adjacent task view is visible on screen.
+ TaskView adjacentTask1 = mRecentsView.getTaskViewAt(runningTaskIndex + 1);
+ TaskView adjacentTask2 = mRecentsView.getTaskViewAt(runningTaskIndex - 1);
+ float prevTranslationX = mRecentsView.getTranslationX();
+ mRecentsView.setTranslationX(0);
+ animate = (adjacentTask1 != null && adjacentTask1.getGlobalVisibleRect(TEMP_RECT))
+ || (adjacentTask2 != null && adjacentTask2.getGlobalVisibleRect(TEMP_RECT));
+ mRecentsView.setTranslationX(prevTranslationX);
+ }
+ }
+ mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
+ }
+
+ public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
+ if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
+ mIsLikelyToStartNewTask = isLikelyToStartNewTask;
+ maybeUpdateRecentsAttachedState();
+ }
+ }
+
+ @UiThread
+ public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
+ mAnimationFactory.setShelfState(shelfState, interpolator, duration);
+ boolean wasShelfPeeking = mIsShelfPeeking;
+ mIsShelfPeeking = shelfState == PEEK;
+ if (mIsShelfPeeking != wasShelfPeeking) {
+ maybeUpdateRecentsAttachedState();
+ }
+ if (mRecentsView != null && shelfState.shouldPreformHaptic) {
+ mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+ }
+
+ private void buildAnimationController() {
+ if (mGestureEndTarget == HOME || mHasLauncherTransitionControllerStarted) {
+ // We don't want a new mLauncherTransitionController if mGestureEndTarget == HOME (it
+ // has its own animation) or if we're already animating the current controller.
+ return;
+ }
+ initTransitionEndpoints(mActivity.getDeviceProfile());
+ mAnimationFactory.createActivityController(mTransitionDragLength);
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
+ WindowInsets result = view.onApplyWindowInsets(windowInsets);
+ buildAnimationController();
+ return result;
+ }
+
+ private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
+ mLauncherTransitionController = anim;
+ mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
+ mAnimationFactory.adjustActivityControllerInterpolators();
+ mLauncherTransitionController.dispatchOnStart();
+ updateLauncherTransitionProgress();
+ }
+
+ @UiThread
+ private void updateFinalShift() {
+ float shift = mCurrentShift.value;
+
+ SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController();
+ if (controller != null) {
+ float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
+ float offsetScale = getTaskCurveScaleForOffsetX(offsetX,
+ mClipAnimationHelper.getTargetRect().width());
+ mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale);
+ mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
+ mTransformParams);
+ updateSysUiFlags(shift);
+ }
+
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (mRecentsAnimationWrapper.getController() != null) {
+ mLiveTileOverlay.update(mClipAnimationHelper.getCurrentRectWithInsets(),
+ mClipAnimationHelper.getCurrentCornerRadius());
+ }
+ }
+
+ final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
+ if (passed != mPassedOverviewThreshold) {
+ mPassedOverviewThreshold = passed;
+ if (mRecentsView != null && mMode != Mode.NO_BUTTON) {
+ mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+ }
+
+ if (mLauncherTransitionController == null || mLauncherTransitionController
+ .getAnimationPlayer().isStarted()) {
+ return;
+ }
+ updateLauncherTransitionProgress();
+ }
+
+ private void updateLauncherTransitionProgress() {
+ if (mGestureEndTarget == HOME) {
+ return;
+ }
+ // Normalize the progress to 0 to 1, as the animation controller will clamp it to that
+ // anyway. The controller mimics the drag length factor by applying it to its interpolators.
+ float progress = mCurrentShift.value / mDragLengthFactor;
+ mLauncherTransitionController.setPlayFraction(
+ progress <= mShiftAtGestureStart || mShiftAtGestureStart >= 1
+ ? 0 : (progress - mShiftAtGestureStart) / (1 - mShiftAtGestureStart));
+ }
+
+ /**
+ * @param windowProgress 0 == app, 1 == overview
+ */
+ private void updateSysUiFlags(float windowProgress) {
+ if (mRecentsView != null) {
+ TaskView centermostTask = mRecentsView.getTaskViewAt(mRecentsView
+ .getPageNearestToCenterOfScreen());
+ int centermostTaskFlags = centermostTask == null ? 0
+ : centermostTask.getThumbnail().getSysUiStatusNavFlags();
+ boolean useHomeScreenFlags = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
+ // We will handle the sysui flags based on the centermost task view.
+ mRecentsAnimationWrapper.setWindowThresholdCrossed(centermostTaskFlags != 0
+ || useHomeScreenFlags);
+ int sysuiFlags = useHomeScreenFlags ? 0 : centermostTaskFlags;
+ mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
+ }
+ }
+
+ @Override
+ public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
+ DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
+ final Rect overviewStackBounds;
+ RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId);
+
+ if (targetSet.minimizedHomeBounds != null && runningTaskTarget != null) {
+ overviewStackBounds = mActivityControlHelper
+ .getOverviewWindowBounds(targetSet.minimizedHomeBounds, runningTaskTarget);
+ dp = dp.getMultiWindowProfile(mContext, new Point(
+ targetSet.minimizedHomeBounds.width(), targetSet.minimizedHomeBounds.height()));
+ dp.updateInsets(targetSet.homeContentInsets);
+ } else {
+ if (mActivity != null) {
+ int loc[] = new int[2];
+ View rootView = mActivity.getRootView();
+ rootView.getLocationOnScreen(loc);
+ overviewStackBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(),
+ loc[1] + rootView.getHeight());
+ } else {
+ overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx);
+ }
+ // If we are not in multi-window mode, home insets should be same as system insets.
+ dp = dp.copy(mContext);
+ dp.updateInsets(targetSet.homeContentInsets);
+ }
+ dp.updateIsSeascape(mContext.getSystemService(WindowManager.class));
+
+ if (runningTaskTarget != null) {
+ mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
+ }
+ mClipAnimationHelper.prepareAnimation(dp, false /* isOpening */);
+ initTransitionEndpoints(dp);
+
+ mRecentsAnimationWrapper.setController(targetSet);
+ TOUCH_INTERACTION_LOG.addLog("startRecentsAnimationCallback", targetSet.apps.length);
+ setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
+
+ mPassedOverviewThreshold = false;
+ }
+
+ @Override
+ public void onRecentsAnimationCanceled() {
+ mRecentsAnimationWrapper.setController(null);
+ mActivityInitListener.unregister();
+ setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
+ TOUCH_INTERACTION_LOG.addLog("cancelRecentsAnimation");
+ }
+
+ @UiThread
+ public void onGestureStarted() {
+ notifyGestureStartedAsync();
+ mShiftAtGestureStart = mCurrentShift.value;
+ setStateOnUiThread(STATE_GESTURE_STARTED);
+ mGestureStarted = true;
+ }
+
+ /**
+ * Notifies the launcher that the swipe gesture has started. This can be called multiple times.
+ */
+ @UiThread
+ private void notifyGestureStartedAsync() {
+ final T curActivity = mActivity;
+ if (curActivity != null) {
+ // Once the gesture starts, we can no longer transition home through the button, so
+ // reset the force override of the activity visibility
+ mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+ }
+ }
+
+ /**
+ * Called as a result on ACTION_CANCEL to return the UI to the start state.
+ */
+ @UiThread
+ public void onGestureCancelled() {
+ updateDisplacement(0);
+ setStateOnUiThread(STATE_GESTURE_COMPLETED);
+ mLogAction = Touch.SWIPE_NOOP;
+ handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */);
+ }
+
+ /**
+ * @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, PointF downPos) {
+ float flingThreshold = mContext.getResources()
+ .getDimension(R.dimen.quickstep_fling_threshold_velocity);
+ boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
+ setStateOnUiThread(STATE_GESTURE_COMPLETED);
+
+ mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
+ boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x);
+ if (isVelocityVertical) {
+ mLogDirection = velocity.y < 0 ? Direction.UP : Direction.DOWN;
+ } else {
+ mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT;
+ }
+ mDownPos = downPos;
+ handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
+ }
+
+ @UiThread
+ private InputConsumer createNewInputProxyHandler() {
+ endRunningWindowAnim();
+ endLauncherTransitionController();
+ if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ // Hide the task view, if not already hidden
+ setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
+ }
+
+ BaseDraggingActivity activity = mActivityControlHelper.getCreatedActivity();
+ return activity == null
+ ? InputConsumer.NO_OP : new OverviewInputConsumer(activity, null, true);
+ }
+
+ private void endRunningWindowAnim() {
+ if (mRunningWindowAnim != null) {
+ mRunningWindowAnim.end();
+ }
+ }
+
+ private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling,
+ boolean isCancel) {
+ final GestureEndTarget endTarget;
+ final boolean goingToNewTask;
+ if (mRecentsView != null) {
+ if (!mRecentsAnimationWrapper.hasTargets()) {
+ // If there are no running tasks, then we can assume that this is a continuation of
+ // the last gesture, but after the recents animation has finished
+ goingToNewTask = true;
+ } else {
+ final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
+ final int taskToLaunch = mRecentsView.getNextPage();
+ goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
+ }
+ } else {
+ goingToNewTask = false;
+ }
+ final boolean reachedOverviewThreshold = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
+ if (!isFling) {
+ if (isCancel) {
+ endTarget = LAST_TASK;
+ } else if (mMode == Mode.NO_BUTTON) {
+ if (mIsShelfPeeking) {
+ endTarget = RECENTS;
+ } else if (goingToNewTask) {
+ endTarget = NEW_TASK;
+ } else {
+ endTarget = !reachedOverviewThreshold ? LAST_TASK : HOME;
+ }
+ } else {
+ endTarget = reachedOverviewThreshold && mGestureStarted
+ ? RECENTS
+ : goingToNewTask
+ ? NEW_TASK
+ : LAST_TASK;
+ }
+ } else {
+ if (mMode == Mode.NO_BUTTON && endVelocity < 0 && !mIsShelfPeeking) {
+ // If swiping at a diagonal, base end target on the faster velocity.
+ endTarget = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity)
+ ? NEW_TASK : HOME;
+ } else if (endVelocity < 0) {
+ if (reachedOverviewThreshold) {
+ endTarget = RECENTS;
+ } else {
+ // If swiping at a diagonal, base end target on the faster velocity.
+ endTarget = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity)
+ ? NEW_TASK : RECENTS;
+ }
+ } else {
+ endTarget = goingToNewTask ? NEW_TASK : LAST_TASK;
+ }
+ }
+
+ int stateFlags = OverviewInteractionState.INSTANCE.get(mActivity).getSystemUiStateFlags();
+ if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0
+ && (endTarget == RECENTS || endTarget == LAST_TASK)) {
+ return LAST_TASK;
+ }
+ return endTarget;
+ }
+
+ @UiThread
+ private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity,
+ boolean isCancel) {
+ PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000);
+ long duration = MAX_SWIPE_DURATION;
+ float currentShift = mCurrentShift.value;
+ final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity,
+ isFling, isCancel);
+ float endShift = endTarget.endShift;
+ final float startShift;
+ Interpolator interpolator = DEACCEL;
+ if (!isFling) {
+ long expectedDuration = Math.abs(Math.round((endShift - currentShift)
+ * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
+ duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
+ startShift = currentShift;
+ interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL;
+ } else {
+ startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y
+ * SINGLE_FRAME_MS / mTransitionDragLength, 0, mDragLengthFactor);
+ float minFlingVelocity = mContext.getResources()
+ .getDimension(R.dimen.quickstep_fling_min_velocity);
+ if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
+ if (endTarget == RECENTS && mMode != Mode.NO_BUTTON) {
+ Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
+ startShift, endShift, endShift, velocityPxPerMs.y,
+ mTransitionDragLength);
+ endShift = overshoot.end;
+ interpolator = overshoot.interpolator;
+ duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION,
+ MAX_SWIPE_DURATION);
+ } else {
+ float distanceToTravel = (endShift - currentShift) * mTransitionDragLength;
+
+ // we want the page's snap velocity to approximately match the velocity at
+ // which the user flings, so we scale the duration by a value near to the
+ // derivative of the scroll interpolator at zero, ie. 2.
+ long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
+ duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
+
+ if (endTarget == RECENTS) {
+ interpolator = OVERSHOOT_1_2;
+ }
+ }
+ }
+ }
+
+ if (endTarget.isLauncher) {
+ mRecentsAnimationWrapper.enableInputProxy();
+ }
+
+ if (endTarget == HOME) {
+ setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
+ duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
+ } else if (endTarget == RECENTS) {
+ mLiveTileOverlay.startIconAnimation();
+ if (mRecentsView != null) {
+ int nearestPage = mRecentsView.getPageNearestToCenterOfScreen();
+ if (mRecentsView.getNextPage() != nearestPage) {
+ // We shouldn't really scroll to the next page when swiping up to recents.
+ // Only allow settling on the next page if it's nearest to the center.
+ mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration));
+ }
+ if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) {
+ mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION);
+ }
+ duration = Math.max(duration, mRecentsView.getScroller().getDuration());
+ }
+ if (mMode == Mode.NO_BUTTON) {
+ setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration);
+ }
+ } else if (endTarget == NEW_TASK || endTarget == LAST_TASK) {
+ // Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
+ // or resumeLastTask().
+ if (mRecentsView != null) {
+ duration = Math.max(duration, mRecentsView.getScroller().getDuration());
+ }
+ }
+ animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
+ }
+
+ private void doLogGesture(GestureEndTarget endTarget) {
+ DeviceProfile dp = mDp;
+ if (dp == null || mDownPos == null) {
+ // We probably never received an animation controller, skip logging.
+ return;
+ }
+
+ int pageIndex = endTarget == LAST_TASK
+ ? LOG_NO_OP_PAGE_INDEX
+ : mRecentsView.getNextPage();
+ UserEventDispatcher.newInstance(mContext).logStateChangeAction(
+ mLogAction, mLogDirection,
+ (int) mDownPos.x, (int) mDownPos.y,
+ ContainerType.NAVBAR, ContainerType.APP,
+ endTarget.containerType,
+ pageIndex);
+ }
+
+ /** Animates to the given progress, where 0 is the current app and 1 is overview. */
+ @UiThread
+ private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
+ GestureEndTarget target, PointF velocityPxPerMs) {
+ mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration,
+ interpolator, target, velocityPxPerMs));
+ }
+
+ @UiThread
+ private void animateToProgressInternal(float start, float end, long duration,
+ Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
+ mGestureEndTarget = target;
+
+ maybeUpdateRecentsAttachedState();
+
+ if (mGestureEndTarget == HOME) {
+ HomeAnimationFactory homeAnimFactory;
+ if (mActivity != null) {
+ homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity);
+ } else {
+ homeAnimFactory = new HomeAnimationFactory() {
+ @NonNull
+ @Override
+ public RectF getWindowTargetRect() {
+ RectF fallbackTarget = new RectF(mClipAnimationHelper.getTargetRect());
+ Utilities.scaleRectFAboutCenter(fallbackTarget, 0.25f);
+ return fallbackTarget;
+ }
+
+ @NonNull
+ @Override
+ public AnimatorPlaybackController createActivityAnimationToHome() {
+ return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
+ }
+ };
+ mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+ isPresent -> mRecentsView.startHome());
+ }
+ RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
+ windowAnim.addAnimatorListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ setStateOnUiThread(target.endState);
+ }
+ });
+ windowAnim.start(velocityPxPerMs);
+ homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
+ mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+ mLauncherTransitionController = null;
+ } else {
+ ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
+ windowAnim.setDuration(duration).setInterpolator(interpolator);
+ windowAnim.addUpdateListener(valueAnimator -> {
+ if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) {
+ // Views typically don't compute scroll when invisible as an optimization,
+ // but in our case we need to since the window offset depends on the scroll.
+ mRecentsView.computeScroll();
+ }
+ });
+ windowAnim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ setStateOnUiThread(target.endState);
+ }
+ });
+ windowAnim.start();
+ mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+ }
+ // Always play the entire launcher animation when going home, since it is separate from
+ // the animation that has been controlled thus far.
+ if (mGestureEndTarget == HOME) {
+ start = 0;
+ }
+
+ // We want to use the same interpolator as the window, but need to adjust it to
+ // interpolate over the remaining progress (end - start).
+ TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress(
+ interpolator, start, end);
+ if (mLauncherTransitionController == null) {
+ return;
+ }
+ if (start == end || duration <= 0) {
+ mLauncherTransitionController.dispatchSetInterpolator(t -> end);
+ } else {
+ mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
+ mAnimationFactory.adjustActivityControllerInterpolators();
+ }
+ mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
+
+ if (QUICKSTEP_SPRINGS.get()) {
+ mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y);
+ }
+ mLauncherTransitionController.getAnimationPlayer().start();
+ mHasLauncherTransitionControllerStarted = true;
+ }
+
+ /**
+ * Creates an animation that transforms the current app window into the home app.
+ * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
+ * @param homeAnimationFactory The home animation factory.
+ */
+ private RectFSpringAnim createWindowAnimationToHome(float startProgress,
+ HomeAnimationFactory homeAnimationFactory) {
+ final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet;
+ final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
+ mTransformParams.setProgress(startProgress), false /* launcherOnTop */));
+ final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
+
+ final View floatingView = homeAnimationFactory.getFloatingView();
+ final boolean isFloatingIconView = floatingView instanceof FloatingIconView;
+ RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mActivity.getResources());
+ if (isFloatingIconView) {
+ FloatingIconView fiv = (FloatingIconView) floatingView;
+ anim.addAnimatorListener(fiv);
+ fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
+ }
+
+ AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome();
+
+ // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
+ // rounding at the end of the animation.
+ float startRadius = mClipAnimationHelper.getCurrentCornerRadius();
+ float endRadius = startRect.width() / 6f;
+ // We want the window alpha to be 0 once this threshold is met, so that the
+ // FolderIconView can be seen morphing into the icon shape.
+ final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f;
+ anim.addOnUpdateListener((currentRect, progress) -> {
+ homeAnim.setPlayFraction(progress);
+
+ float alphaProgress = ACCEL_1_5.getInterpolation(progress);
+ float windowAlpha = Utilities.boundToRange(Utilities.mapToRange(alphaProgress, 0,
+ windowAlphaThreshold, 1.5f, 0f, Interpolators.LINEAR), 0, 1);
+ mTransformParams.setProgress(progress)
+ .setCurrentRectAndTargetAlpha(currentRect, windowAlpha);
+ if (isFloatingIconView) {
+ mTransformParams.setCornerRadius(endRadius * progress + startRadius
+ * (1f - progress));
+ }
+ mClipAnimationHelper.applyTransform(targetSet, mTransformParams,
+ false /* launcherOnTop */);
+
+ if (isFloatingIconView) {
+ ((FloatingIconView) floatingView).update(currentRect, 1f, progress,
+ windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(), false);
+ }
+
+ updateSysUiFlags(Math.max(progress, mCurrentShift.value));
+ });
+ anim.addAnimatorListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ homeAnim.dispatchOnStart();
+ if (mActivity != null) {
+ mActivity.getRootView().getOverlay().remove(mLiveTileOverlay);
+ }
+ }
+
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ homeAnim.getAnimationPlayer().end();
+ if (mRecentsView != null) {
+ mRecentsView.post(mRecentsView::resetTaskVisuals);
+ }
+ // Make sure recents is in its final state
+ maybeUpdateRecentsAttachedState(false);
+ mActivityControlHelper.onSwipeUpToHomeComplete(mActivity);
+ }
+ });
+ return anim;
+ }
+
+ /**
+ * @return The GestureEndTarget if the gesture has ended, else null.
+ */
+ public @Nullable GestureEndTarget getGestureEndTarget() {
+ return mGestureEndTarget;
+ }
+
+ @UiThread
+ private void resumeLastTask() {
+ mRecentsAnimationWrapper.finish(false /* toRecents */, null);
+ TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", false);
+ doLogGesture(LAST_TASK);
+ reset();
+ }
+
+ @UiThread
+ private void startNewTask() {
+ // Launch the task user scrolled to (mRecentsView.getNextPage()).
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ // We finish recents animation inside launchTask() when live tile is enabled.
+ mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false /* animate */,
+ true /* freezeTaskList */);
+ } else {
+ int taskId = mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).getTask().key.id;
+ mFinishingRecentsAnimationForNewTaskId = taskId;
+ mRecentsAnimationWrapper.finish(true /* toRecents */, () -> {
+ if (!mCanceled) {
+ TaskView nextTask = mRecentsView.getTaskView(taskId);
+ if (nextTask != null) {
+ nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
+ success -> {
+ if (!success) {
+ // We couldn't launch the task, so take user to overview so they can
+ // decide what to do instead of staying in this broken state.
+ endLauncherTransitionController();
+ mActivityControlHelper.onLaunchTaskFailed(mActivity);
+ nextTask.notifyTaskLaunchFailed(TAG);
+ updateSysUiFlags(1 /* windowProgress == overview */);
+ } else {
+ mActivityControlHelper.onLaunchTaskSuccess(mActivity);
+ }
+ }, mMainThreadHandler);
+ doLogGesture(NEW_TASK);
+ }
+ reset();
+ }
+ mCanceled = false;
+ mFinishingRecentsAnimationForNewTaskId = -1;
+ });
+ }
+ TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
+ }
+
+ public void reset() {
+ setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+ }
+
+ /**
+ * Cancels any running animation so that the active target can be overriden by a new swipe
+ * handle (in case of quick switch).
+ */
+ public void cancelCurrentAnimation(SwipeSharedState sharedState) {
+ mCanceled = true;
+ mCurrentShift.cancelAnimation();
+ if (mLauncherTransitionController != null && mLauncherTransitionController
+ .getAnimationPlayer().isStarted()) {
+ mLauncherTransitionController.getAnimationPlayer().cancel();
+ }
+
+ if (mFinishingRecentsAnimationForNewTaskId != -1) {
+ // If we are canceling mid-starting a new task, switch to the screenshot since the
+ // recents animation has finished
+ switchToScreenshot();
+ TaskView newRunningTaskView = mRecentsView.getTaskView(
+ mFinishingRecentsAnimationForNewTaskId);
+ int newRunningTaskId = newRunningTaskView != null
+ ? newRunningTaskView.getTask().key.id
+ : -1;
+ mRecentsView.setCurrentTask(newRunningTaskId);
+ sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId);
+ }
+ }
+
+ private void invalidateHandler() {
+ endRunningWindowAnim();
+
+ if (mGestureEndCallback != null) {
+ mGestureEndCallback.run();
+ }
+
+ mActivityInitListener.unregister();
+ mTaskSnapshot = null;
+ }
+
+ private void invalidateHandlerWithLauncher() {
+ endLauncherTransitionController();
+
+ mRecentsView.onGestureAnimationEnd();
+
+ mActivity.getRootView().setOnApplyWindowInsetsListener(null);
+ mActivity.getRootView().getOverlay().remove(mLiveTileOverlay);
+ }
+
+ private void endLauncherTransitionController() {
+ setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
+ if (mLauncherTransitionController != null) {
+ mLauncherTransitionController.getAnimationPlayer().end();
+ mLauncherTransitionController = null;
+ }
+ }
+
+ private void notifyTransitionCancelled() {
+ mAnimationFactory.onTransitionCancelled();
+ }
+
+ private void resetStateForAnimationCancel() {
+ boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
+ mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible);
+
+ // Leave the pending invisible flag, as it may be used by wallpaper open animation.
+ mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
+ }
+
+ private void switchToScreenshot() {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+ } else if (!mRecentsAnimationWrapper.hasTargets()) {
+ // If there are no targets, then we don't need to capture anything
+ setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+ } else {
+ boolean finishTransitionPosted = false;
+ SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController();
+ if (controller != null) {
+ // Update the screenshot of the task
+ if (mTaskSnapshot == null) {
+ mTaskSnapshot = controller.screenshotTask(mRunningTaskId);
+ }
+ final TaskView taskView;
+ if (mGestureEndTarget == HOME) {
+ // Capture the screenshot before finishing the transition to home to ensure it's
+ // taken in the correct orientation, but no need to update the thumbnail.
+ taskView = null;
+ } else {
+ taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot);
+ }
+ if (taskView != null && !mCanceled) {
+ // Defer finishing the animation until the next launcher frame with the
+ // new thumbnail
+ finishTransitionPosted = new WindowCallbacksCompat(taskView) {
+
+ // The number of frames to defer until we actually finish the animation
+ private int mDeferFrameCount = 2;
+
+ @Override
+ public void onPostDraw(Canvas canvas) {
+ // If we were cancelled after this was attached, do not update
+ // the state.
+ if (mCanceled) {
+ detach();
+ return;
+ }
+
+ if (mDeferFrameCount > 0) {
+ mDeferFrameCount--;
+ // Workaround, detach and reattach to invalidate the root node for
+ // another draw
+ detach();
+ attach();
+ taskView.invalidate();
+ return;
+ }
+
+ setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+ detach();
+ }
+ }.attach();
+ }
+ }
+ if (!finishTransitionPosted) {
+ // If we haven't posted a draw callback, set the state immediately.
+ RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, ENTER);
+ setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+ RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, EXIT);
+ }
+ }
+ }
+
+ private void finishCurrentTransitionToRecents() {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+ } else if (!mRecentsAnimationWrapper.hasTargets()) {
+ // If there are no targets, then there is nothing to finish
+ setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+ } else {
+ synchronized (mRecentsAnimationWrapper) {
+ mRecentsAnimationWrapper.finish(true /* toRecents */,
+ () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+ }
+ }
+ TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
+ }
+
+ private void finishCurrentTransitionToHome() {
+ synchronized (mRecentsAnimationWrapper) {
+ mRecentsAnimationWrapper.finish(true /* toRecents */,
+ () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED),
+ true /* sendUserLeaveHint */);
+ }
+ TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
+ doLogGesture(HOME);
+ }
+
+ private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
+ endLauncherTransitionController();
+ mActivityControlHelper.onSwipeUpToRecentsComplete(mActivity);
+ mRecentsAnimationWrapper.setCancelWithDeferredScreenshot(true);
+ mRecentsView.onSwipeUpAnimationSuccess();
+
+ RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG);
+
+ doLogGesture(RECENTS);
+ reset();
+ }
+
+ public void setGestureEndCallback(Runnable gestureEndCallback) {
+ mGestureEndCallback = gestureEndCallback;
+ }
+
+ private void setTargetAlphaProvider(
+ BiFunction provider) {
+ mClipAnimationHelper.setTaskAlphaCallback(provider);
+ updateFinalShift();
+ }
+
+ public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) {
+ if (!isNotInRecents(app)) {
+ return 0;
+ }
+ return expectedAlpha;
+ }
+
+ private static boolean isNotInRecents(RemoteAnimationTargetCompat app) {
+ return app.isNotInRecents
+ || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
+ }
+
+ private interface RunningWindowAnim {
+ void end();
+
+ static RunningWindowAnim wrap(Animator animator) {
+ return animator::end;
+ }
+
+ static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
+ return rectFSpringAnim::end;
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
new file mode 100644
index 0000000000000000000000000000000000000000..c28761804b4590294f2f3c07e2973c8739206060
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+package com.android.quickstep.fallback;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.view.View;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherState.ScaleAndTranslation;
+import com.android.launcher3.Utilities;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+public class FallbackRecentsView extends RecentsView {
+
+ public static final FloatProperty ZOOM_PROGRESS =
+ new FloatProperty ("zoomInProgress") {
+
+ @Override
+ public void setValue(FallbackRecentsView view, float value) {
+ view.setZoomProgress(value);
+ }
+
+ @Override
+ public Float get(FallbackRecentsView view) {
+ return view.mZoomInProgress;
+ }
+ };
+
+ private float mZoomInProgress = 0;
+ private boolean mInOverviewState = true;
+
+ private float mZoomScale = 1f;
+ private float mZoomTranslationY = 0f;
+
+ public FallbackRecentsView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setOverviewStateEnabled(true);
+ setOverlayEnabled(true);
+ }
+
+ @Override
+ public void startHome() {
+ mActivity.startHome();
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ updateEmptyMessage();
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+ updateEmptyMessage();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ maybeDrawEmptyMessage(canvas);
+ super.draw(canvas);
+ }
+
+ @Override
+ protected void getTaskSize(DeviceProfile dp, Rect outRect) {
+ LayoutUtils.calculateFallbackTaskSize(getContext(), dp, outRect);
+ }
+
+ @Override
+ public boolean shouldUseMultiWindowTaskSizeStrategy() {
+ // Just use the activity task size for multi-window as well.
+ return false;
+ }
+
+ public void resetViewUI() {
+ setZoomProgress(0);
+ resetTaskVisuals();
+ }
+
+ public void setInOverviewState(boolean inOverviewState) {
+ if (mInOverviewState != inOverviewState) {
+ mInOverviewState = inOverviewState;
+ if (mInOverviewState) {
+ resetTaskVisuals();
+ } else {
+ setZoomProgress(1);
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (getTaskViewCount() == 0) {
+ mZoomScale = 1f;
+ mZoomTranslationY = 0f;
+ } else {
+ TaskView dummyTask = getTaskViewAt(0);
+ ScaleAndTranslation sat = getTempClipAnimationHelper()
+ .updateForFullscreenOverview(dummyTask)
+ .getScaleAndTranslation();
+ mZoomScale = sat.scale;
+ mZoomTranslationY = sat.translationY;
+ }
+
+ setZoomProgress(mZoomInProgress);
+ }
+
+ public void setZoomProgress(float progress) {
+ mZoomInProgress = progress;
+ SCALE_PROPERTY.set(this, Utilities.mapRange(mZoomInProgress, 1, mZoomScale));
+ TRANSLATION_Y.set(this, Utilities.mapRange(mZoomInProgress, 0, mZoomTranslationY));
+ FULLSCREEN_PROGRESS.set(this, mZoomInProgress);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
similarity index 88%
rename from quickstep/src/com/android/quickstep/fallback/RecentsRootView.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
index ca8c2520c4db2ad51d27c850525fcca3e47fffad..18207295767595be44ef381dc576a63b81b1b85b 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
@@ -30,13 +30,14 @@ import com.android.quickstep.RecentsActivity;
public class RecentsRootView extends BaseDragLayer {
+ private static final int MIN_SIZE = 10;
private final RecentsActivity mActivity;
- private final Point mLastKnownSize = new Point(10, 10);
+ private final Point mLastKnownSize = new Point(MIN_SIZE, MIN_SIZE);
public RecentsRootView(Context context, AttributeSet attrs) {
super(context, attrs, 1 /* alphaChannelCount */);
- mActivity = (RecentsActivity) BaseActivity.fromContext(context);
+ mActivity = BaseActivity.fromContext(context);
setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| SYSTEM_UI_FLAG_LAYOUT_STABLE);
@@ -53,8 +54,8 @@ public class RecentsRootView extends BaseDragLayer {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Check size changes before the actual measure, to avoid multiple measure calls.
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
+ int width = Math.max(MIN_SIZE, MeasureSpec.getSize(widthMeasureSpec));
+ int height = Math.max(MIN_SIZE, MeasureSpec.getSize(heightMeasureSpec));
if (mLastKnownSize.x != width || mLastKnownSize.y != height) {
mLastKnownSize.set(width, height);
mActivity.onRootViewSizeChanged();
@@ -69,7 +70,7 @@ public class RecentsRootView extends BaseDragLayer {
// Update device profile before notifying the children.
mActivity.getDeviceProfile().updateInsets(insets);
setInsets(insets);
- return true; // I'll take it from here
+ return false; // Let children get the full insets
}
@Override
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
similarity index 92%
rename from quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
index 9463cc90f1ced11092181cb37c9b50ce468010e2..a113604caa6959fd7ac8fec41d3e2c79558bdbf6 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
@@ -15,7 +15,7 @@
*/
package com.android.quickstep.fallback;
-import com.android.launcher3.uioverrides.TaskViewTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
import com.android.quickstep.RecentsActivity;
public class RecentsTaskController extends TaskViewTouchController {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f73a28eff71d9690f003d44aca2827a6ba92672
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -0,0 +1,161 @@
+/*
+ * 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.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_POINTER_UP;
+import static android.view.MotionEvent.ACTION_UP;
+
+import android.content.Context;
+import android.graphics.RectF;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.R;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Touch consumer for two finger swipe actions for accessibility actions
+ */
+public class AccessibilityInputConsumer extends DelegateInputConsumer {
+
+ private static final String TAG = "A11yInputConsumer";
+
+ private final ISystemUiProxy mSystemUiProxy;
+ private final VelocityTracker mVelocityTracker;
+ private final MotionPauseDetector mMotionPauseDetector;
+ private final boolean mAllowLongClick;
+ private final RectF mSwipeTouchRegion;
+
+ private final float mMinGestureDistance;
+ private final float mMinFlingVelocity;
+
+ private int mActivePointerId = -1;
+ private float mDownY;
+ private float mTotalY;
+
+ public AccessibilityInputConsumer(Context context, ISystemUiProxy systemUiProxy,
+ boolean allowLongClick, InputConsumer delegate, InputMonitorCompat inputMonitor,
+ RectF swipeTouchRegion) {
+ super(delegate, inputMonitor);
+ mSystemUiProxy = systemUiProxy;
+ mVelocityTracker = VelocityTracker.obtain();
+ mMinGestureDistance = context.getResources()
+ .getDimension(R.dimen.accessibility_gesture_min_swipe_distance);
+ mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+ mSwipeTouchRegion = swipeTouchRegion;
+
+ mMotionPauseDetector = new MotionPauseDetector(context);
+ mAllowLongClick = allowLongClick;
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_ACCESSIBILITY | mDelegate.getType();
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (mState != STATE_DELEGATE_ACTIVE) {
+ mVelocityTracker.addMovement(ev);
+ }
+
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ break;
+ }
+ case ACTION_POINTER_UP: {
+ if (mState == STATE_ACTIVE) {
+ int pointerIndex = ev.getActionIndex();
+ int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ final int newPointerIdx = pointerIndex == 0 ? 1 : 0;
+
+ mTotalY += (ev.getY(pointerIndex) - mDownY);
+ mDownY = ev.getY(newPointerIdx);
+ mActivePointerId = ev.getPointerId(newPointerIdx);
+ }
+ }
+ break;
+ }
+ case ACTION_POINTER_DOWN: {
+ if (mState == STATE_INACTIVE) {
+ int pointerIndex = ev.getActionIndex();
+ if (mSwipeTouchRegion.contains(ev.getX(pointerIndex), ev.getY(pointerIndex))
+ && mDelegate.allowInterceptByParent()) {
+ setActive(ev);
+
+ mActivePointerId = ev.getPointerId(pointerIndex);
+ mDownY = ev.getY(pointerIndex);
+ } else {
+ mState = STATE_DELEGATE_ACTIVE;
+ }
+ }
+ break;
+ }
+ case ACTION_MOVE: {
+ if (mState == STATE_ACTIVE && mAllowLongClick) {
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ break;
+ }
+
+ mMotionPauseDetector.addPosition(ev.getY(pointerIndex) - mDownY,
+ ev.getEventTime());
+ }
+ break;
+ }
+ case ACTION_UP:
+ if (mState == STATE_ACTIVE) {
+ try {
+ if (mAllowLongClick && mMotionPauseDetector.isPaused()) {
+ mSystemUiProxy.notifyAccessibilityButtonLongClicked();
+ } else {
+ mTotalY += (ev.getY() - mDownY);
+ mVelocityTracker.computeCurrentVelocity(1000);
+
+ if ((-mTotalY) > mMinGestureDistance
+ || (-mVelocityTracker.getYVelocity()) > mMinFlingVelocity) {
+ mSystemUiProxy.notifyAccessibilityButtonClicked(
+ Display.DEFAULT_DISPLAY);
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to notify accessibility event", e);
+ }
+ }
+ // Follow through
+ case ACTION_CANCEL: {
+ mVelocityTracker.recycle();
+ mMotionPauseDetector.clear();
+ break;
+ }
+ }
+
+ if (mState != STATE_ACTIVE) {
+ mDelegate.onMotionEvent(ev);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantTouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantTouchConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..38b5a137c40fcf97af22a5fcb1579438654eb980
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantTouchConsumer.java
@@ -0,0 +1,294 @@
+/*
+ * 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.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_POINTER_UP;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+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.FLING;
+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;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+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.ActivityControlHelper;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Touch consumer for handling events to launch assistant from launcher
+ */
+public class AssistantTouchConsumer extends DelegateInputConsumer {
+
+ private static final String TAG = "AssistantTouchConsumer";
+ private static final long RETRACT_ANIMATION_DURATION_MS = 300;
+
+ // From //java/com/google/android/apps/gsa/search/shared/util/OpaContract.java.
+ private static final String OPA_BUNDLE_TRIGGER = "triggered_by";
+ // From //java/com/google/android/apps/gsa/assistant/shared/proto/opa_trigger.proto.
+ private static final int OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE = 83;
+ private static final String INVOCATION_TYPE_KEY = "invocation_type";
+ private static final int INVOCATION_TYPE_GESTURE = 1;
+
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+ private final PointF mStartDragPos = new PointF();
+
+ private int mActivePointerId = -1;
+ private boolean mPassedSlop;
+ private boolean mLaunchedAssistant;
+ private float mDistance;
+ private float mTimeFraction;
+ private long mDragTime;
+ private float mLastProgress;
+ private int mDirection;
+ private ActivityControlHelper mActivityControlHelper;
+
+ private final float mDistThreshold;
+ private final long mTimeThreshold;
+ private final int mAngleThreshold;
+ private final float mSquaredSlop;
+ private final ISystemUiProxy mSysUiProxy;
+ private final Context mContext;
+ private final GestureDetector mGestureDetector;
+
+ public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy,
+ ActivityControlHelper activityControlHelper, InputConsumer delegate,
+ InputMonitorCompat inputMonitor) {
+ super(delegate, inputMonitor);
+ final Resources res = context.getResources();
+ mContext = context;
+ mSysUiProxy = systemUiProxy;
+ 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);
+
+ float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+ mSquaredSlop = slop * slop;
+ mActivityControlHelper = activityControlHelper;
+
+ mGestureDetector = new GestureDetector(context, new AssistantGestureListener());
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_ASSISTANT | mDelegate.getType();
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ // TODO add logging
+ mGestureDetector.onTouchEvent(ev);
+
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ mActivePointerId = ev.getPointerId(0);
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+ mTimeFraction = 0;
+ break;
+ }
+ case ACTION_POINTER_DOWN: {
+ if (mState != STATE_ACTIVE) {
+ mState = STATE_DELEGATE_ACTIVE;
+ break;
+ }
+ }
+ case ACTION_POINTER_UP: {
+ int ptrIdx = ev.getActionIndex();
+ int ptrId = ev.getPointerId(ptrIdx);
+ if (ptrId == mActivePointerId) {
+ final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+ mDownPos.set(
+ ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+ ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+ mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+ mActivePointerId = ev.getPointerId(newPointerIdx);
+ }
+ break;
+ }
+ case ACTION_MOVE: {
+ if (mState == STATE_DELEGATE_ACTIVE) {
+ break;
+ }
+ if (!mDelegate.allowInterceptByParent()) {
+ mState = STATE_DELEGATE_ACTIVE;
+ break;
+ }
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ break;
+ }
+ mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+ if (!mPassedSlop) {
+ // Normal gesture, ensure we pass the slop before we start tracking the gesture
+ if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+ > mSquaredSlop) {
+
+ mPassedSlop = true;
+ mStartDragPos.set(mLastPos.x, mLastPos.y);
+ mDragTime = SystemClock.uptimeMillis();
+
+ if (isValidAssistantGestureAngle(
+ mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) {
+ setActive(ev);
+ } else {
+ mState = STATE_DELEGATE_ACTIVE;
+ }
+ }
+ } else {
+ // Movement
+ mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
+ mLastPos.y - mStartDragPos.y);
+ if (mDistance >= 0) {
+ final long diff = SystemClock.uptimeMillis() - mDragTime;
+ mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
+ updateAssistantProgress();
+ }
+ }
+ break;
+ }
+ case ACTION_CANCEL:
+ case ACTION_UP:
+ if (mState != STATE_DELEGATE_ACTIVE && !mLaunchedAssistant) {
+ ValueAnimator animator = ValueAnimator.ofFloat(mLastProgress, 0)
+ .setDuration(RETRACT_ANIMATION_DURATION_MS);
+ UserEventDispatcher.newInstance(mContext).logActionOnContainer(
+ SWIPE_NOOP, mDirection, NAVBAR);
+ animator.addUpdateListener(valueAnimator -> {
+ float progress = (float) valueAnimator.getAnimatedValue();
+ try {
+
+ mSysUiProxy.onAssistantProgress(progress);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to send SysUI start/send assistant progress: "
+ + progress, e);
+ }
+ });
+ animator.setInterpolator(Interpolators.DEACCEL_2);
+ animator.start();
+ }
+ mPassedSlop = false;
+ mState = STATE_INACTIVE;
+ break;
+ }
+
+ if (mState != STATE_ACTIVE) {
+ mDelegate.onMotionEvent(ev);
+ }
+ }
+
+ private void updateAssistantProgress() {
+ if (!mLaunchedAssistant) {
+ mLastProgress = Math.min(mDistance * 1f / mDistThreshold, 1) * mTimeFraction;
+ try {
+ if (mDistance >= mDistThreshold && mTimeFraction >= 1) {
+ mSysUiProxy.onAssistantGestureCompletion(0);
+ startAssistantInternal(SWIPE);
+
+ Bundle args = new Bundle();
+ args.putInt(OPA_BUNDLE_TRIGGER, OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE);
+ args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
+ mSysUiProxy.startAssistant(args);
+ mLaunchedAssistant = true;
+ } else {
+ mSysUiProxy.onAssistantProgress(mLastProgress);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to send SysUI start/send assistant progress: " + mLastProgress,
+ e);
+ }
+ }
+ }
+
+ private void startAssistantInternal(int gestureType) {
+ UserEventDispatcher.newInstance(mContext)
+ .logActionOnContainer(gestureType, mDirection, NAVBAR);
+
+ BaseDraggingActivity launcherActivity = mActivityControlHelper
+ .getCreatedActivity();
+ if (launcherActivity != null) {
+ launcherActivity.getRootView().performHapticFeedback(
+ 13, // HapticFeedbackConstants.GESTURE_END
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+ }
+
+ /**
+ * Determine if angle is larger than threshold for assistant detection
+ */
+ private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) {
+ float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+ mDirection = angle > 90 ? UPLEFT : UPRIGHT;
+
+ // normalize so that angle is measured clockwise from horizontal in the bottom right corner
+ // and counterclockwise from horizontal in the bottom left corner
+ angle = angle > 90 ? 180 - angle : angle;
+ return (angle > mAngleThreshold && angle < 90);
+ }
+
+ private class AssistantGestureListener extends SimpleOnGestureListener {
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (isValidAssistantGestureAngle(velocityX, -velocityY)
+ && !mLaunchedAssistant && mState != STATE_DELEGATE_ACTIVE) {
+ mLastProgress = 1;
+ try {
+ mSysUiProxy.onAssistantGestureCompletion(
+ (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY));
+ startAssistantInternal(FLING);
+
+ Bundle args = new Bundle();
+ args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
+ mSysUiProxy.startAssistant(args);
+ mLaunchedAssistant = true;
+ } catch (RemoteException e) {
+ Log.w(TAG,
+ "Failed to send SysUI start/send assistant progress: " + mLastProgress,
+ e);
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..311ddd27caf33290a72ad897b8760cc3e241d135
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -0,0 +1,49 @@
+package com.android.quickstep.inputconsumers;
+
+import android.view.MotionEvent;
+
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+public abstract class DelegateInputConsumer implements InputConsumer {
+
+ protected static final int STATE_INACTIVE = 0;
+ protected static final int STATE_ACTIVE = 1;
+ protected static final int STATE_DELEGATE_ACTIVE = 2;
+
+ protected final InputConsumer mDelegate;
+ protected final InputMonitorCompat mInputMonitor;
+
+ protected int mState;
+
+ public DelegateInputConsumer(InputConsumer delegate, InputMonitorCompat inputMonitor) {
+ mDelegate = delegate;
+ mInputMonitor = inputMonitor;
+ mState = STATE_INACTIVE;
+ }
+
+ @Override
+ public boolean useSharedSwipeState() {
+ return mDelegate.useSharedSwipeState();
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return mDelegate.allowInterceptByParent() && mState != STATE_ACTIVE;
+ }
+
+ @Override
+ public void onConsumerAboutToBeSwitched() {
+ mDelegate.onConsumerAboutToBeSwitched();
+ }
+
+ protected void setActive(MotionEvent ev) {
+ mState = STATE_ACTIVE;
+ mInputMonitor.pilferPointers();
+
+ // Send cancel event
+ MotionEvent event = MotionEvent.obtain(ev);
+ event.setAction(MotionEvent.ACTION_CANCEL);
+ mDelegate.onMotionEvent(event);
+ event.recycle();
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..db2af59aca161b5d937258b89391eb94766cad98
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -0,0 +1,256 @@
+/*
+ * 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.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.quickstep.LockScreenRecentsActivity;
+import com.android.quickstep.MultiStateCallback;
+import com.android.quickstep.SwipeSharedState;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.RecentsAnimationListenerSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+import com.android.systemui.shared.system.InputMonitorCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+/**
+ * A dummy input consumer used when the device is still locked, e.g. from secure camera.
+ */
+public class DeviceLockedInputConsumer implements InputConsumer,
+ SwipeAnimationTargetSet.SwipeAnimationListener {
+
+ private static final float SCALE_DOWN = 0.75f;
+
+ private static final String[] STATE_NAMES = DEBUG_STATES ? new String[2] : null;
+ private static int getFlagForIndex(int index, String name) {
+ if (DEBUG_STATES) {
+ STATE_NAMES[index] = name;
+ }
+ return 1 << index;
+ }
+
+ private static final int STATE_TARGET_RECEIVED =
+ getFlagForIndex(0, "STATE_TARGET_RECEIVED");
+ private static final int STATE_HANDLER_INVALIDATED =
+ getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
+
+ private final Context mContext;
+ private final float mTouchSlopSquared;
+ private final SwipeSharedState mSwipeSharedState;
+ private final InputMonitorCompat mInputMonitorCompat;
+
+ private final PointF mTouchDown = new PointF();
+ private final ClipAnimationHelper mClipAnimationHelper;
+ private final ClipAnimationHelper.TransformParams mTransformParams;
+ private final Point mDisplaySize;
+ private final MultiStateCallback mStateCallback;
+ private final RectF mSwipeTouchRegion;
+ public final int mRunningTaskId;
+
+ private VelocityTracker mVelocityTracker;
+ private float mProgress;
+
+ private boolean mThresholdCrossed = false;
+
+ private SwipeAnimationTargetSet mTargetSet;
+
+ public DeviceLockedInputConsumer(Context context, SwipeSharedState swipeSharedState,
+ InputMonitorCompat inputMonitorCompat, RectF swipeTouchRegion, int runningTaskId) {
+ mContext = context;
+ mTouchSlopSquared = squaredTouchSlop(context);
+ mSwipeSharedState = swipeSharedState;
+ mClipAnimationHelper = new ClipAnimationHelper(context);
+ mTransformParams = new ClipAnimationHelper.TransformParams();
+ mInputMonitorCompat = inputMonitorCompat;
+ mSwipeTouchRegion = swipeTouchRegion;
+ mRunningTaskId = runningTaskId;
+
+ // Do not use DeviceProfile as the user data might be locked
+ mDisplaySize = new Point();
+ context.getSystemService(WindowManager.class).getDefaultDisplay().getRealSize(mDisplaySize);
+
+ // Init states
+ mStateCallback = new MultiStateCallback(STATE_NAMES);
+ mStateCallback.addCallback(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
+ this::endRemoteAnimation);
+
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_DEVICE_LOCKED;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+ mVelocityTracker.addMovement(ev);
+
+ float x = ev.getX();
+ float y = ev.getY();
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mTouchDown.set(x, y);
+ break;
+ case ACTION_POINTER_DOWN: {
+ if (!mThresholdCrossed) {
+ // Cancel interaction in case of multi-touch interaction
+ int ptrIdx = ev.getActionIndex();
+ if (!mSwipeTouchRegion.contains(ev.getX(ptrIdx), ev.getY(ptrIdx))) {
+ int action = ev.getAction();
+ ev.setAction(ACTION_CANCEL);
+ finishTouchTracking(ev);
+ ev.setAction(action);
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (!mThresholdCrossed) {
+ if (squaredHypot(x - mTouchDown.x, y - mTouchDown.y) > mTouchSlopSquared) {
+ startRecentsTransition();
+ }
+ } else {
+ float dy = Math.max(mTouchDown.y - y, 0);
+ mProgress = dy / mDisplaySize.y;
+ mTransformParams.setProgress(mProgress);
+ if (mTargetSet != null) {
+ mClipAnimationHelper.applyTransform(mTargetSet, mTransformParams);
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ finishTouchTracking(ev);
+ break;
+ }
+ }
+
+ /**
+ * Called when the gesture has ended. Does not correlate to the completion of the interaction as
+ * the animation can still be running.
+ */
+ private void finishTouchTracking(MotionEvent ev) {
+ mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+ if (mThresholdCrossed && ev.getAction() == ACTION_UP) {
+ mVelocityTracker.computeCurrentVelocity(1000,
+ ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
+
+ float velocityY = mVelocityTracker.getYVelocity();
+ float flingThreshold = mContext.getResources()
+ .getDimension(R.dimen.quickstep_fling_threshold_velocity);
+
+ boolean dismissTask;
+ if (Math.abs(velocityY) > flingThreshold) {
+ // Is fling
+ dismissTask = velocityY < 0;
+ } else {
+ dismissTask = mProgress >= (1 - MIN_PROGRESS_FOR_OVERVIEW);
+ }
+ if (dismissTask) {
+ // 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));
+ }
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ private void startRecentsTransition() {
+ mThresholdCrossed = true;
+ RecentsAnimationListenerSet newListenerSet =
+ mSwipeSharedState.newRecentsAnimationListenerSet();
+ newListenerSet.addListener(this);
+ Intent intent = new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_DEFAULT)
+ .setComponent(new ComponentName(mContext, LockScreenRecentsActivity.class))
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+ mInputMonitorCompat.pilferPointers();
+ BackgroundExecutor.get().submit(
+ () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+ intent, null, newListenerSet, null, null));
+ }
+
+ @Override
+ public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
+ mTargetSet = targetSet;
+
+ Rect displaySize = new Rect(0, 0, mDisplaySize.x, mDisplaySize.y);
+ RemoteAnimationTargetCompat targetCompat = targetSet.findTask(mRunningTaskId);
+ if (targetCompat != null) {
+ mClipAnimationHelper.updateSource(displaySize, targetCompat);
+ }
+
+ Utilities.scaleRectAboutCenter(displaySize, SCALE_DOWN);
+ displaySize.offsetTo(displaySize.left, 0);
+ mClipAnimationHelper.updateTargetRect(displaySize);
+ mClipAnimationHelper.applyTransform(mTargetSet, mTransformParams);
+
+ mStateCallback.setState(STATE_TARGET_RECEIVED);
+ }
+
+ @Override
+ public void onRecentsAnimationCanceled() {
+ mTargetSet = null;
+ }
+
+ private void endRemoteAnimation() {
+ if (mTargetSet != null) {
+ mTargetSet.finishController(
+ false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */);
+ }
+ }
+
+ @Override
+ public void onConsumerAboutToBeSwitched() {
+ mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mThresholdCrossed;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..d05ca2a1612b0f9d77cbb6e5e273868303c03bc8
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
@@ -0,0 +1,358 @@
+/*
+ * 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.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+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.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION;
+import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.WindowTransformSwipeHandler.MIN_SWIPE_DURATION;
+import static com.android.quickstep.inputconsumers.OtherActivityInputConsumer.QUICKSTEP_TOUCH_SLOP_RATIO;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.quickstep.ActivityControlHelper;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.SwipeSharedState;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.RecentsAnimationListenerSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+import com.android.systemui.shared.system.InputMonitorCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+public class FallbackNoButtonInputConsumer implements InputConsumer, SwipeAnimationListener {
+
+ private static final int STATE_NOT_FINISHED = 0;
+ private static final int STATE_FINISHED_TO_HOME = 1;
+ private static final int STATE_FINISHED_TO_APP = 2;
+
+ private static final float PROGRESS_TO_END_GESTURE = -2;
+
+ private final ActivityControlHelper mActivityControlHelper;
+ private final InputMonitorCompat mInputMonitor;
+ private final Context mContext;
+ private final NavBarPosition mNavBarPosition;
+ private final SwipeSharedState mSwipeSharedState;
+ private final OverviewComponentObserver mOverviewComponentObserver;
+ private final int mRunningTaskId;
+
+ private final ClipAnimationHelper mClipAnimationHelper;
+ private final TransformParams mTransformParams = new TransformParams();
+ private final float mTransitionDragLength;
+ private final DeviceProfile mDP;
+
+ private final RectF mSwipeTouchRegion;
+ private final boolean mDisableHorizontalSwipe;
+
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+
+ private int mActivePointerId = -1;
+ // Slop used to determine when we say that the gesture has started.
+ private boolean mPassedPilferInputSlop;
+
+ private VelocityTracker mVelocityTracker;
+
+ // Distance after which we start dragging the window.
+ private final float mTouchSlop;
+
+ // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
+ private float mStartDisplacement;
+ private SwipeAnimationTargetSet mSwipeAnimationTargetSet;
+ private float mProgress;
+
+ private int mState = STATE_NOT_FINISHED;
+
+ public FallbackNoButtonInputConsumer(Context context,
+ ActivityControlHelper activityControlHelper, InputMonitorCompat inputMonitor,
+ SwipeSharedState swipeSharedState, RectF swipeTouchRegion,
+ OverviewComponentObserver overviewComponentObserver,
+ boolean disableHorizontalSwipe, RunningTaskInfo runningTaskInfo) {
+ mContext = context;
+ mActivityControlHelper = activityControlHelper;
+ mInputMonitor = inputMonitor;
+ mOverviewComponentObserver = overviewComponentObserver;
+ mRunningTaskId = runningTaskInfo.id;
+
+ mSwipeSharedState = swipeSharedState;
+ mSwipeTouchRegion = swipeTouchRegion;
+ mDisableHorizontalSwipe = disableHorizontalSwipe;
+
+ mNavBarPosition = new NavBarPosition(context);
+ mVelocityTracker = VelocityTracker.obtain();
+
+ mTouchSlop = QUICKSTEP_TOUCH_SLOP_RATIO
+ * ViewConfiguration.get(context).getScaledTouchSlop();
+
+ mClipAnimationHelper = new ClipAnimationHelper(context);
+
+ mDP = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context).copy(context);
+ Rect tempRect = new Rect();
+ mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
+ mDP, context, tempRect);
+ mClipAnimationHelper.updateTargetRect(tempRect);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_FALLBACK_NO_BUTTON;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+
+ mVelocityTracker.addMovement(ev);
+ if (ev.getActionMasked() == ACTION_POINTER_UP) {
+ mVelocityTracker.clear();
+ }
+
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ mActivePointerId = ev.getPointerId(0);
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+ break;
+ }
+ case ACTION_POINTER_DOWN: {
+ if (!mPassedPilferInputSlop) {
+ // Cancel interaction in case of multi-touch interaction
+ int ptrIdx = ev.getActionIndex();
+ if (!mSwipeTouchRegion.contains(ev.getX(ptrIdx), ev.getY(ptrIdx))) {
+ forceCancelGesture(ev);
+ }
+ }
+ break;
+ }
+ case ACTION_POINTER_UP: {
+ int ptrIdx = ev.getActionIndex();
+ int ptrId = ev.getPointerId(ptrIdx);
+ if (ptrId == mActivePointerId) {
+ final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+ mDownPos.set(
+ ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+ ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+ mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+ mActivePointerId = ev.getPointerId(newPointerIdx);
+ }
+ break;
+ }
+ case ACTION_MOVE: {
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == INVALID_POINTER_ID) {
+ break;
+ }
+ mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+ float displacement = getDisplacement(ev);
+
+ if (!mPassedPilferInputSlop) {
+ if (mDisableHorizontalSwipe && Math.abs(mLastPos.x - mDownPos.x)
+ > Math.abs(mLastPos.y - mDownPos.y)) {
+ // Horizontal gesture is not allowed in this region
+ forceCancelGesture(ev);
+ break;
+ }
+
+ if (Math.abs(displacement) >= mTouchSlop) {
+ mPassedPilferInputSlop = true;
+
+ // Deferred gesture, start the animation and gesture tracking once
+ // we pass the actual touch slop
+ startTouchTrackingForWindowAnimation(displacement);
+ }
+ } else {
+ updateDisplacement(displacement - mStartDisplacement);
+ }
+ break;
+ }
+ case ACTION_CANCEL:
+ case ACTION_UP: {
+ finishTouchTracking(ev);
+ break;
+ }
+ }
+ }
+
+ private void startTouchTrackingForWindowAnimation(float displacement) {
+ mStartDisplacement = Math.min(displacement, -mTouchSlop);
+
+ RecentsAnimationListenerSet listenerSet =
+ mSwipeSharedState.newRecentsAnimationListenerSet();
+ listenerSet.addListener(this);
+ Intent homeIntent = mOverviewComponentObserver.getHomeIntent();
+ BackgroundExecutor.get().submit(
+ () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+ homeIntent, null, listenerSet, null, null));
+
+ ActivityManagerWrapper.getInstance().closeSystemWindows(
+ CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ mInputMonitor.pilferPointers();
+ }
+
+ private void updateDisplacement(float displacement) {
+ mProgress = displacement / mTransitionDragLength;
+ mTransformParams.setProgress(mProgress);
+
+ if (mSwipeAnimationTargetSet != null) {
+ mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
+ }
+ }
+
+ private void forceCancelGesture(MotionEvent ev) {
+ int action = ev.getAction();
+ ev.setAction(ACTION_CANCEL);
+ finishTouchTracking(ev);
+ ev.setAction(action);
+ }
+
+ /**
+ * Called when the gesture has ended. Does not correlate to the completion of the interaction as
+ * the animation can still be running.
+ */
+ private void finishTouchTracking(MotionEvent ev) {
+ if (ev.getAction() == ACTION_CANCEL) {
+ mState = STATE_FINISHED_TO_APP;
+ } else {
+ mVelocityTracker.computeCurrentVelocity(1000,
+ ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
+ float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
+ float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
+ float velocity = mNavBarPosition.isRightEdge() ? velocityX
+ : mNavBarPosition.isLeftEdge() ? -velocityX
+ : velocityY;
+ float flingThreshold = mContext.getResources()
+ .getDimension(R.dimen.quickstep_fling_threshold_velocity);
+ boolean isFling = Math.abs(velocity) > flingThreshold;
+
+ boolean goingHome;
+ if (!isFling) {
+ goingHome = -mProgress >= MIN_PROGRESS_FOR_OVERVIEW;
+ } else {
+ goingHome = velocity < 0;
+ }
+
+ if (goingHome) {
+ mState = STATE_FINISHED_TO_HOME;
+ } else {
+ mState = STATE_FINISHED_TO_APP;
+ }
+ }
+
+ if (mSwipeAnimationTargetSet != null) {
+ finishAnimationTargetSet();
+ }
+ }
+
+ private void finishAnimationTargetSet() {
+ if (mState == STATE_FINISHED_TO_APP) {
+ mSwipeAnimationTargetSet.finishController(false, null, false);
+ } else {
+ if (mProgress < PROGRESS_TO_END_GESTURE) {
+ mSwipeAnimationTargetSet.finishController(true, null, true);
+ } else {
+ long duration = (long) (Math.min(mProgress - PROGRESS_TO_END_GESTURE, 1)
+ * MAX_SWIPE_DURATION / Math.abs(PROGRESS_TO_END_GESTURE));
+ if (duration < 0) {
+ duration = MIN_SWIPE_DURATION;
+ }
+
+ ValueAnimator anim = ValueAnimator.ofFloat(mProgress, PROGRESS_TO_END_GESTURE);
+ anim.addUpdateListener(a -> {
+ float p = (Float) anim.getAnimatedValue();
+ mTransformParams.setProgress(p);
+ mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
+ });
+ anim.setDuration(duration);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mSwipeAnimationTargetSet.finishController(true, null, true);
+ }
+ });
+ anim.start();
+ }
+ }
+ }
+
+ @Override
+ public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
+ mSwipeAnimationTargetSet = targetSet;
+ Rect overviewStackBounds = new Rect(0, 0, mDP.widthPx, mDP.heightPx);
+ RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId);
+
+ mDP.updateIsSeascape(mContext.getSystemService(WindowManager.class));
+ if (runningTaskTarget != null) {
+ mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
+ }
+ mClipAnimationHelper.prepareAnimation(mDP, false /* isOpening */);
+
+ overviewStackBounds
+ .inset(-overviewStackBounds.width() / 5, -overviewStackBounds.height() / 5);
+ mClipAnimationHelper.updateTargetRect(overviewStackBounds);
+ mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
+
+ if (mState != STATE_NOT_FINISHED) {
+ finishAnimationTargetSet();
+ }
+ }
+
+ @Override
+ public void onRecentsAnimationCanceled() { }
+
+ private float getDisplacement(MotionEvent ev) {
+ if (mNavBarPosition.isRightEdge()) {
+ return ev.getX() - mDownPos.x;
+ } else if (mNavBarPosition.isLeftEdge()) {
+ return mDownPos.x - ev.getX();
+ } else {
+ return ev.getY() - mDownPos.y;
+ }
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mPassedPilferInputSlop;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..f5cf654b15a5f47d4605ce23b7f9f92f3c42ea25
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package com.android.quickstep.inputconsumers;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+@TargetApi(Build.VERSION_CODES.O)
+public interface InputConsumer {
+
+ int TYPE_NO_OP = 1 << 0;
+ int TYPE_OVERVIEW = 1 << 1;
+ int TYPE_OTHER_ACTIVITY = 1 << 2;
+ int TYPE_ASSISTANT = 1 << 3;
+ int TYPE_DEVICE_LOCKED = 1 << 4;
+ int TYPE_ACCESSIBILITY = 1 << 5;
+ int TYPE_SCREEN_PINNED = 1 << 6;
+ int TYPE_OVERVIEW_WITHOUT_FOCUS = 1 << 7;
+ int TYPE_RESET_GESTURE = 1 << 8;
+ int TYPE_FALLBACK_NO_BUTTON = 1 << 9;
+
+ String[] NAMES = new String[] {
+ "TYPE_NO_OP", // 0
+ "TYPE_OVERVIEW", // 1
+ "TYPE_OTHER_ACTIVITY", // 2
+ "TYPE_ASSISTANT", // 3
+ "TYPE_DEVICE_LOCKED", // 4
+ "TYPE_ACCESSIBILITY", // 5
+ "TYPE_SCREEN_PINNED", // 6
+ "TYPE_OVERVIEW_WITHOUT_FOCUS", // 7
+ "TYPE_RESET_GESTURE", // 8
+ "TYPE_FALLBACK_NO_BUTTON", // 9
+ };
+
+ InputConsumer NO_OP = () -> TYPE_NO_OP;
+
+ int getType();
+
+ default boolean useSharedSwipeState() {
+ return false;
+ }
+
+ /**
+ * Returns true if the user has crossed the threshold for it to be an explicit action.
+ */
+ default boolean allowInterceptByParent() {
+ return true;
+ }
+
+ /**
+ * Called by the event queue when the consumer is about to be switched to a new consumer.
+ */
+ default void onConsumerAboutToBeSwitched() { }
+
+ default void onMotionEvent(MotionEvent ev) { }
+
+ default void onKeyEvent(KeyEvent ev) { }
+
+ default void onInputEvent(InputEvent ev) {
+ if (ev instanceof MotionEvent) {
+ onMotionEvent((MotionEvent) ev);
+ } else {
+ onKeyEvent((KeyEvent) ev);
+ }
+ }
+
+ default String getName() {
+ String name = "";
+ for (int i = 0; i < NAMES.length; i++) {
+ if ((getType() & (1 << i)) != 0) {
+ if (name.length() > 0) {
+ name += ":";
+ }
+ name += NAMES[i];
+ }
+ }
+ return name;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c137d3bf9ec06c9c4f9b3e02a3195c4568dd838
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -0,0 +1,457 @@
+/*
+ * 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.
+ */
+package com.android.quickstep.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+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.Utilities.squaredHypot;
+import static com.android.launcher3.util.RaceConditionTracker.ENTER;
+import static com.android.launcher3.util.RaceConditionTracker.EXIT;
+import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.RaceConditionTracker;
+import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.ActivityControlHelper;
+import com.android.quickstep.OverviewCallbacks;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SwipeSharedState;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.WindowTransformSwipeHandler;
+import com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget;
+import com.android.quickstep.util.CachedEventDispatcher;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.RecentsAnimationListenerSet;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+import java.util.function.Consumer;
+
+import androidx.annotation.UiThread;
+
+/**
+ * Input consumer for handling events originating from an activity other than Launcher
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer {
+
+ public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN";
+ private static final String UP_EVT = "OtherActivityInputConsumer.UP";
+
+ // TODO: Move to quickstep contract
+ public static final float QUICKSTEP_TOUCH_SLOP_RATIO = 3;
+
+ private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
+ private final RunningTaskInfo mRunningTask;
+ private final RecentsModel mRecentsModel;
+ private final Intent mHomeIntent;
+ private final ActivityControlHelper mActivityControlHelper;
+ private final OverviewCallbacks mOverviewCallbacks;
+ private final InputConsumerController mInputConsumer;
+ private final SwipeSharedState mSwipeSharedState;
+ private final InputMonitorCompat mInputMonitorCompat;
+ private final SysUINavigationMode.Mode mMode;
+ private final RectF mSwipeTouchRegion;
+
+ private final NavBarPosition mNavBarPosition;
+
+ private final Consumer mOnCompleteCallback;
+ private final MotionPauseDetector mMotionPauseDetector;
+ private final float mMotionPauseMinDisplacement;
+ private VelocityTracker mVelocityTracker;
+
+ private WindowTransformSwipeHandler mInteractionHandler;
+
+ private final boolean mIsDeferredDownTarget;
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+ private int mActivePointerId = INVALID_POINTER_ID;
+
+ // Distance after which we start dragging the window.
+ private final float mTouchSlop;
+
+ private final float mSquaredTouchSlop;
+ private final boolean mDisableHorizontalSwipe;
+
+ // Slop used to check when we start moving window.
+ private boolean mPaddedWindowMoveSlop;
+ // Slop used to determine when we say that the gesture has started.
+ private boolean mPassedPilferInputSlop;
+
+ // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
+ private float mStartDisplacement;
+
+ private Handler mMainThreadHandler;
+ private Runnable mCancelRecentsAnimationRunnable = () -> {
+ ActivityManagerWrapper.getInstance().cancelRecentsAnimation(
+ true /* restoreHomeStackPosition */);
+ };
+
+ public OtherActivityInputConsumer(Context base, RunningTaskInfo runningTaskInfo,
+ RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl,
+ boolean isDeferredDownTarget, OverviewCallbacks overviewCallbacks,
+ InputConsumerController inputConsumer,
+ Consumer onCompleteCallback,
+ SwipeSharedState swipeSharedState, InputMonitorCompat inputMonitorCompat,
+ RectF swipeTouchRegion, boolean disableHorizontalSwipe) {
+ super(base);
+
+ mMainThreadHandler = new Handler(Looper.getMainLooper());
+ mRunningTask = runningTaskInfo;
+ mRecentsModel = recentsModel;
+ mHomeIntent = homeIntent;
+ mMode = SysUINavigationMode.getMode(base);
+ mSwipeTouchRegion = swipeTouchRegion;
+
+ mMotionPauseDetector = new MotionPauseDetector(base);
+ mMotionPauseMinDisplacement = base.getResources().getDimension(
+ R.dimen.motion_pause_detector_min_displacement_from_app);
+ mOnCompleteCallback = onCompleteCallback;
+ mVelocityTracker = VelocityTracker.obtain();
+ mInputMonitorCompat = inputMonitorCompat;
+
+ mActivityControlHelper = activityControl;
+ boolean continuingPreviousGesture = swipeSharedState.getActiveListener() != null;
+ mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
+ mOverviewCallbacks = overviewCallbacks;
+ mInputConsumer = inputConsumer;
+ mSwipeSharedState = swipeSharedState;
+
+ mNavBarPosition = new NavBarPosition(base);
+ mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+
+ float slop = QUICKSTEP_TOUCH_SLOP_RATIO * mTouchSlop;
+ mSquaredTouchSlop = slop * slop;
+
+ mPassedPilferInputSlop = mPaddedWindowMoveSlop = continuingPreviousGesture;
+ mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_OTHER_ACTIVITY;
+ }
+
+ private void forceCancelGesture(MotionEvent ev) {
+ int action = ev.getAction();
+ ev.setAction(ACTION_CANCEL);
+ finishTouchTracking(ev);
+ ev.setAction(action);
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+
+ // Proxy events to recents view
+ if (mPaddedWindowMoveSlop && mInteractionHandler != null
+ && !mRecentsViewDispatcher.hasConsumer()) {
+ mRecentsViewDispatcher.setConsumer(mInteractionHandler.getRecentsViewDispatcher(
+ mNavBarPosition.getRotationMode()));
+ }
+ int edgeFlags = ev.getEdgeFlags();
+ ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
+ mRecentsViewDispatcher.dispatchEvent(ev);
+ ev.setEdgeFlags(edgeFlags);
+
+ mVelocityTracker.addMovement(ev);
+ if (ev.getActionMasked() == ACTION_POINTER_UP) {
+ mVelocityTracker.clear();
+ mMotionPauseDetector.clear();
+ }
+
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ RaceConditionTracker.onEvent(DOWN_EVT, ENTER);
+ TraceHelper.beginSection("TouchInt");
+ mActivePointerId = ev.getPointerId(0);
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+
+ // Start the window animation on down to give more time for launcher to draw if the
+ // user didn't start the gesture over the back button
+ if (!mIsDeferredDownTarget) {
+ startTouchTrackingForWindowAnimation(ev.getEventTime());
+ }
+
+ RaceConditionTracker.onEvent(DOWN_EVT, EXIT);
+ break;
+ }
+ case ACTION_POINTER_DOWN: {
+ if (!mPassedPilferInputSlop) {
+ // Cancel interaction in case of multi-touch interaction
+ int ptrIdx = ev.getActionIndex();
+ if (!mSwipeTouchRegion.contains(ev.getX(ptrIdx), ev.getY(ptrIdx))) {
+ forceCancelGesture(ev);
+ }
+ }
+ break;
+ }
+ case ACTION_POINTER_UP: {
+ int ptrIdx = ev.getActionIndex();
+ int ptrId = ev.getPointerId(ptrIdx);
+ if (ptrId == mActivePointerId) {
+ final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+ mDownPos.set(
+ ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+ ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+ mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+ mActivePointerId = ev.getPointerId(newPointerIdx);
+ }
+ break;
+ }
+ case ACTION_MOVE: {
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == INVALID_POINTER_ID) {
+ break;
+ }
+ mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+ float displacement = getDisplacement(ev);
+ float displacementX = mLastPos.x - mDownPos.x;
+
+ if (!mPaddedWindowMoveSlop) {
+ if (!mIsDeferredDownTarget) {
+ // Normal gesture, ensure we pass the drag slop before we start tracking
+ // the gesture
+ if (Math.abs(displacement) > mTouchSlop) {
+ mPaddedWindowMoveSlop = true;
+ mStartDisplacement = Math.min(displacement, -mTouchSlop);
+ }
+ }
+ }
+
+ if (!mPassedPilferInputSlop) {
+ float displacementY = mLastPos.y - mDownPos.y;
+ if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
+ if (mDisableHorizontalSwipe
+ && Math.abs(displacementX) > Math.abs(displacementY)) {
+ // Horizontal gesture is not allowed in this region
+ forceCancelGesture(ev);
+ break;
+ }
+
+ mPassedPilferInputSlop = true;
+
+ if (mIsDeferredDownTarget) {
+ // Deferred gesture, start the animation and gesture tracking once
+ // we pass the actual touch slop
+ startTouchTrackingForWindowAnimation(ev.getEventTime());
+ }
+ if (!mPaddedWindowMoveSlop) {
+ mPaddedWindowMoveSlop = true;
+ mStartDisplacement = Math.min(displacement, -mTouchSlop);
+
+ }
+ notifyGestureStarted();
+ }
+ }
+
+ if (mInteractionHandler != null) {
+ if (mPaddedWindowMoveSlop) {
+ // Move
+ mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
+ }
+
+ if (mMode == Mode.NO_BUTTON) {
+ float horizontalDist = Math.abs(displacementX);
+ float upDist = -displacement;
+ boolean isLikelyToStartNewTask = horizontalDist > upDist;
+ mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
+ || isLikelyToStartNewTask);
+ mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
+ mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
+ }
+ }
+ break;
+ }
+ case ACTION_CANCEL:
+ case ACTION_UP: {
+ finishTouchTracking(ev);
+ break;
+ }
+ }
+ }
+
+ private void notifyGestureStarted() {
+ TOUCH_INTERACTION_LOG.addLog("startQuickstep");
+ if (mInteractionHandler == null) {
+ return;
+ }
+ mInputMonitorCompat.pilferPointers();
+
+ mOverviewCallbacks.closeAllWindows();
+ ActivityManagerWrapper.getInstance().closeSystemWindows(
+ CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+
+ // Notify the handler that the gesture has actually started
+ mInteractionHandler.onGestureStarted();
+ }
+
+ private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
+ TOUCH_INTERACTION_LOG.addLog("startRecentsAnimation");
+
+ RecentsAnimationListenerSet listenerSet = mSwipeSharedState.getActiveListener();
+ final WindowTransformSwipeHandler handler = new WindowTransformSwipeHandler(
+ mRunningTask, this, touchTimeMs, mActivityControlHelper,
+ listenerSet != null, mInputConsumer);
+
+ // Preload the plan
+ mRecentsModel.getTasks(null);
+ mInteractionHandler = handler;
+ handler.setGestureEndCallback(this::onInteractionGestureFinished);
+ mMotionPauseDetector.setOnMotionPauseListener(handler::onMotionPauseChanged);
+ handler.initWhenReady();
+
+ if (listenerSet != null) {
+ listenerSet.addListener(handler);
+ mSwipeSharedState.applyActiveRecentsAnimationState(handler);
+ notifyGestureStarted();
+ } else {
+ RecentsAnimationListenerSet newListenerSet =
+ mSwipeSharedState.newRecentsAnimationListenerSet();
+ newListenerSet.addListener(handler);
+ BackgroundExecutor.get().submit(
+ () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+ mHomeIntent, null, newListenerSet, null, null));
+ }
+ }
+
+ /**
+ * Called when the gesture has ended. Does not correlate to the completion of the interaction as
+ * the animation can still be running.
+ */
+ private void finishTouchTracking(MotionEvent ev) {
+ RaceConditionTracker.onEvent(UP_EVT, ENTER);
+ TraceHelper.endSection("TouchInt");
+
+ if (mPaddedWindowMoveSlop && mInteractionHandler != null) {
+ if (ev.getActionMasked() == ACTION_CANCEL) {
+ mInteractionHandler.onGestureCancelled();
+ } else {
+ mVelocityTracker.computeCurrentVelocity(1000,
+ ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
+ float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
+ float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
+ float velocity = mNavBarPosition.isRightEdge() ? velocityX
+ : mNavBarPosition.isLeftEdge() ? -velocityX
+ : velocityY;
+
+ mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
+ 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.
+ onConsumerAboutToBeSwitched();
+ onInteractionGestureFinished();
+
+ // Cancel the recents animation if SysUI happens to handle UP before we have a chance
+ // to start the recents animation. In addition, workaround for b/126336729 by delaying
+ // the cancel of the animation for a period, in case SysUI is slow to handle UP and we
+ // handle DOWN & UP and move the home stack before SysUI can start the activity
+ mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable);
+ mMainThreadHandler.postDelayed(mCancelRecentsAnimationRunnable, 100);
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ mMotionPauseDetector.clear();
+ RaceConditionTracker.onEvent(UP_EVT, EXIT);
+ }
+
+ @Override
+ public void onConsumerAboutToBeSwitched() {
+ Preconditions.assertUIThread();
+ mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable);
+ if (mInteractionHandler != null) {
+ // The consumer is being switched while we are active. Set up the shared state to be
+ // used by the next animation
+ removeListener();
+ GestureEndTarget endTarget = mInteractionHandler.getGestureEndTarget();
+ mSwipeSharedState.canGestureBeContinued = endTarget != null && endTarget.canBeContinued;
+ mSwipeSharedState.goingToLauncher = endTarget != null && endTarget.isLauncher;
+ if (mSwipeSharedState.canGestureBeContinued) {
+ mInteractionHandler.cancelCurrentAnimation(mSwipeSharedState);
+ } else {
+ mInteractionHandler.reset();
+ }
+ }
+ }
+
+ @UiThread
+ private void onInteractionGestureFinished() {
+ Preconditions.assertUIThread();
+ removeListener();
+ mInteractionHandler = null;
+ mOnCompleteCallback.accept(this);
+ }
+
+ private void removeListener() {
+ RecentsAnimationListenerSet listenerSet = mSwipeSharedState.getActiveListener();
+ if (listenerSet != null) {
+ listenerSet.removeListener(mInteractionHandler);
+ }
+ }
+
+ private float getDisplacement(MotionEvent ev) {
+ if (mNavBarPosition.isRightEdge()) {
+ return ev.getX() - mDownPos.x;
+ } else if (mNavBarPosition.isLeftEdge()) {
+ return mDownPos.x - ev.getX();
+ } else {
+ return ev.getY() - mDownPos.y;
+ }
+ }
+
+ @Override
+ public boolean useSharedSwipeState() {
+ return mInteractionHandler != null;
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mPassedPilferInputSlop;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..b021df877ea326f8c71f1ca5a9eac8217a23689b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -0,0 +1,117 @@
+/*
+ * 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.inputconsumers;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.OverviewCallbacks;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+import java.util.function.Predicate;
+
+/**
+ * Input consumer for handling touch on the recents/Launcher activity.
+ */
+public class OverviewInputConsumer
+ implements InputConsumer {
+
+ private final T mActivity;
+ private final BaseDragLayer mTarget;
+ private final InputMonitorCompat mInputMonitor;
+
+ private final int[] mLocationOnScreen = new int[2];
+ private final boolean mProxyTouch;
+ private final Predicate mEventReceiver;
+
+ private final boolean mStartingInActivityBounds;
+ private boolean mTargetHandledTouch;
+
+ public OverviewInputConsumer(T activity, @Nullable InputMonitorCompat inputMonitor,
+ boolean startingInActivityBounds) {
+ mActivity = activity;
+ mInputMonitor = inputMonitor;
+ mStartingInActivityBounds = startingInActivityBounds;
+
+ mTarget = activity.getDragLayer();
+ if (startingInActivityBounds) {
+ mEventReceiver = mTarget::dispatchTouchEvent;
+ mProxyTouch = true;
+ } else {
+ // Only proxy touches to controllers if we are starting touch from nav bar.
+ mEventReceiver = mTarget::proxyTouchEvent;
+ mTarget.getLocationOnScreen(mLocationOnScreen);
+ mProxyTouch = mTarget.prepareProxyEventStarting();
+ }
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_OVERVIEW;
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mTargetHandledTouch;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (!mProxyTouch) {
+ return;
+ }
+
+ int flags = ev.getEdgeFlags();
+ if (!mStartingInActivityBounds) {
+ ev.setEdgeFlags(flags | Utilities.EDGE_NAV_BAR);
+ }
+ ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
+ boolean handled = mEventReceiver.test(ev);
+ ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
+ ev.setEdgeFlags(flags);
+
+ if (!mTargetHandledTouch && handled) {
+ mTargetHandledTouch = true;
+ if (!mStartingInActivityBounds) {
+ OverviewCallbacks.get(mActivity).closeAllWindows();
+ ActivityManagerWrapper.getInstance()
+ .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ TOUCH_INTERACTION_LOG.addLog("startQuickstep");
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.pilferPointers();
+ }
+ }
+ }
+
+ @Override
+ public void onKeyEvent(KeyEvent ev) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mActivity.dispatchKeyEvent(ev);
+ }
+ }
+}
+
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..425b8b6b0b6e256e2c76c9001cd5927324e36e48
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -0,0 +1,151 @@
+/*
+ * 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.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.Utilities;
+import com.android.quickstep.OverviewCallbacks;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+public class OverviewWithoutFocusInputConsumer implements InputConsumer {
+
+ private final InputMonitorCompat mInputMonitor;
+ private final boolean mDisableHorizontalSwipe;
+ private final PointF mDownPos = new PointF();
+ private final float mSquaredTouchSlop;
+ private final Context mContext;
+ private final NavBarPosition mNavBarPosition;
+
+ private boolean mInterceptedTouch;
+ private VelocityTracker mVelocityTracker;
+
+
+ public OverviewWithoutFocusInputConsumer(Context context, InputMonitorCompat inputMonitor,
+ boolean disableHorizontalSwipe) {
+ mInputMonitor = inputMonitor;
+ mDisableHorizontalSwipe = disableHorizontalSwipe;
+ mContext = context;
+ mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+ mNavBarPosition = new NavBarPosition(context);
+
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_OVERVIEW_WITHOUT_FOCUS;
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mInterceptedTouch;
+ }
+
+ private void endTouchTracking() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+
+ mVelocityTracker.addMovement(ev);
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ mDownPos.set(ev.getX(), ev.getY());
+ break;
+ }
+ case ACTION_MOVE: {
+ if (!mInterceptedTouch) {
+ float displacementX = ev.getX() - mDownPos.x;
+ float displacementY = ev.getY() - mDownPos.y;
+ if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
+ if (mDisableHorizontalSwipe
+ && Math.abs(displacementX) > Math.abs(displacementY)) {
+ // Horizontal gesture is not allowed in this region
+ endTouchTracking();
+ break;
+ }
+
+ mInterceptedTouch = true;
+
+ if (mInputMonitor != null) {
+ mInputMonitor.pilferPointers();
+ }
+ }
+ }
+ break;
+ }
+
+ case ACTION_CANCEL:
+ endTouchTracking();
+ break;
+
+ case ACTION_UP: {
+ finishTouchTracking(ev);
+ endTouchTracking();
+ break;
+ }
+ }
+ }
+
+ private void finishTouchTracking(MotionEvent ev) {
+ mVelocityTracker.computeCurrentVelocity(100);
+ float velocityX = mVelocityTracker.getXVelocity();
+ float velocityY = mVelocityTracker.getYVelocity();
+ float velocity = mNavBarPosition.isRightEdge()
+ ? -velocityX : (mNavBarPosition.isLeftEdge() ? velocityX : -velocityY);
+
+ final boolean triggerQuickstep;
+ if (Math.abs(velocity) >= ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity()) {
+ triggerQuickstep = velocity > 0;
+ } else {
+ float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
+ float displacementY = ev.getY() - mDownPos.y;
+ triggerQuickstep = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
+ }
+
+ if (triggerQuickstep) {
+ OverviewCallbacks.get(mContext).closeAllWindows();
+ ActivityManagerWrapper.getInstance()
+ .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ TOUCH_INTERACTION_LOG.addLog("startQuickstep");
+ } else {
+ // ignore
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..8eede81b846ffa22ee15655c7bb5140c95c5cc6a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
@@ -0,0 +1,45 @@
+/*
+ * 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.inputconsumers;
+
+import android.view.MotionEvent;
+
+import com.android.quickstep.SwipeSharedState;
+
+/**
+ * A NO_OP input consumer which also resets any pending gesture
+ */
+public class ResetGestureInputConsumer implements InputConsumer {
+
+ private final SwipeSharedState mSwipeSharedState;
+
+ public ResetGestureInputConsumer(SwipeSharedState swipeSharedState) {
+ mSwipeSharedState = swipeSharedState;
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_RESET_GESTURE;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN
+ && mSwipeSharedState.getActiveListener() != null) {
+ mSwipeSharedState.clearAllState(false /* finishAnimation */);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0e20f2cd85ca785bfc7d5a832def670447b75ca
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
@@ -0,0 +1,88 @@
+/*
+ * 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.inputconsumers;
+
+import android.content.Context;
+import android.os.RemoteException;
+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.quickstep.ActivityControlHelper;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+
+/**
+ * An input consumer that detects swipe up and hold to exit screen pinning mode.
+ */
+public class ScreenPinnedInputConsumer implements InputConsumer {
+
+ private static final String TAG = "ScreenPinnedConsumer";
+
+ private final float mMotionPauseMinDisplacement;
+ private final MotionPauseDetector mMotionPauseDetector;
+
+ private float mTouchDownY;
+
+ public ScreenPinnedInputConsumer(Context context, ISystemUiProxy sysuiProxy,
+ ActivityControlHelper activityControl) {
+ mMotionPauseMinDisplacement = context.getResources().getDimension(
+ R.dimen.motion_pause_detector_min_displacement_from_app);
+ mMotionPauseDetector = new MotionPauseDetector(context, true /* makePauseHarderToTrigger*/);
+ mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
+ if (isPaused) {
+ try {
+ sysuiProxy.stopScreenPinning();
+ BaseDraggingActivity launcherActivity = activityControl.getCreatedActivity();
+ if (launcherActivity != null) {
+ launcherActivity.getRootView().performHapticFeedback(
+ HapticFeedbackConstants.LONG_PRESS,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+ mMotionPauseDetector.clear();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to stop screen pinning ", e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_SCREEN_PINNED;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ float y = ev.getY();
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mTouchDownY = y;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ float displacement = mTouchDownY - y;
+ mMotionPauseDetector.setDisallowPause(displacement < mMotionPauseMinDisplacement);
+ mMotionPauseDetector.addPosition(y, ev.getEventTime());
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ mMotionPauseDetector.clear();
+ break;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
similarity index 51%
rename from quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
index df70a8a394ea8b53b932537a40ce1a51d4a6b179..6dc672ecfa77dc161de93032491958583234bcf6 100644
--- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -15,42 +15,41 @@
*/
package com.android.quickstep.util;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
+import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
import android.annotation.TargetApi;
-import android.graphics.Canvas;
+import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Matrix.ScaleToFit;
-import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.RemoteException;
-import android.support.annotation.Nullable;
-import android.view.Surface;
-import android.view.animation.Interpolator;
+
+import androidx.annotation.Nullable;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.recents.utilities.RectFEvaluator;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier.SurfaceParams;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
import com.android.systemui.shared.system.TransactionCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;
-import java.util.function.BiConsumer;
import java.util.function.BiFunction;
/**
@@ -67,36 +66,49 @@ public class ClipAnimationHelper {
private final RectF mSourceRect = new RectF();
// The bounds of the task view in launcher window coordinates
private final RectF mTargetRect = new RectF();
- // Set when the final window destination is changed, such as offsetting for quick scrub
- private final PointF mTargetOffset = new PointF();
// The insets to be used for clipping the app window, which can be larger than mSourceInsets
// if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
// app window coordinates.
private final RectF mSourceWindowClipInsets = new RectF();
+ // The insets to be used for clipping the app window. For live tile, we don't transform the clip
+ // relative to the target rect.
+ private final RectF mSourceWindowClipInsetsForLiveTile = new RectF();
// The bounds of launcher (not including insets) in device coordinates
public final Rect mHomeStackBounds = new Rect();
// The clip rect in source app window coordinates
- private final Rect mClipRect = new Rect();
+ private final RectF mClipRectF = new RectF();
private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
private final Matrix mTmpMatrix = new Matrix();
+ private final Rect mTmpRect = new Rect();
private final RectF mTmpRectF = new RectF();
-
- private float mTargetScale = 1f;
- private float mOffsetScale = 1f;
- private Interpolator mInterpolator = LINEAR;
- // We translate y slightly faster than the rest of the animation for quick scrub.
- private Interpolator mOffsetYInterpolator = LINEAR;
+ private final RectF mCurrentRectWithInsets = new RectF();
+ // Corner radius of windows, in pixels
+ private final float mWindowCornerRadius;
+ // Corner radius of windows when they're in overview mode.
+ private final float mTaskCornerRadius;
+ // If windows can have real time rounded corners.
+ private final boolean mSupportsRoundedCornersOnWindows;
+ // Whether or not to actually use the rounded cornders on windows
+ private boolean mUseRoundedCornersOnWindows;
+
+ // Corner radius currently applied to transformed window.
+ private float mCurrentCornerRadius;
// Whether to boost the opening animation target layers, or the closing
private int mBoostModeTargetLayers = -1;
- // Wether or not applyTransform has been called yet since prepareAnimation()
- private boolean mIsFirstFrame = true;
private BiFunction mTaskAlphaCallback =
(t, a1) -> a1;
+ public ClipAnimationHelper(Context context) {
+ mWindowCornerRadius = getWindowCornerRadius(context.getResources());
+ mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources());
+ mTaskCornerRadius = TaskCornerRadius.get(context);
+ mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows;
+ }
+
private void updateSourceStack(RemoteAnimationTargetCompat target) {
mSourceInsets.set(target.contentInsets);
mSourceStackBounds.set(target.sourceContainerBounds);
@@ -111,13 +123,11 @@ public class ClipAnimationHelper {
updateSourceStack(target);
}
- public void updateTargetRect(TransformedRect targetRect) {
- mOffsetScale = targetRect.scale;
+ public void updateTargetRect(Rect targetRect) {
mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
mSourceStackBounds.width() - mSourceInsets.right,
mSourceStackBounds.height() - mSourceInsets.bottom);
- mTargetRect.set(targetRect.rect);
- Utilities.scaleRectFAboutCenter(mTargetRect, targetRect.scale);
+ mTargetRect.set(targetRect);
mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
mHomeStackBounds.top - mSourceStackBounds.top);
@@ -134,71 +144,103 @@ public class ClipAnimationHelper {
Math.max(scaledTargetRect.top, 0),
Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
+ mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets);
mSourceRect.set(scaledTargetRect);
}
- public void prepareAnimation(boolean isOpening) {
+ public void prepareAnimation(DeviceProfile dp, boolean isOpening) {
mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
+ mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows && !dp.isMultiWindowMode;
}
- public RectF applyTransform(RemoteAnimationTargetSet targetSet, float progress,
- @Nullable SyncRtSurfaceTransactionApplier syncTransactionApplier) {
- RectF currentRect;
- mTmpRectF.set(mTargetRect);
- Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale);
- float offsetYProgress = mOffsetYInterpolator.getInterpolation(progress);
- progress = mInterpolator.getInterpolation(progress);
- currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
-
- synchronized (mTargetOffset) {
- // Stay lined up with the center of the target, since it moves for quick scrub.
- currentRect.offset(mTargetOffset.x * mOffsetScale * progress,
- mTargetOffset.y * offsetYProgress);
- }
+ public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) {
+ return applyTransform(targetSet, params, true /* launcherOnTop */);
+ }
- mClipRect.left = (int) (mSourceWindowClipInsets.left * progress);
- mClipRect.top = (int) (mSourceWindowClipInsets.top * progress);
- mClipRect.right = (int)
- (mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress));
- mClipRect.bottom = (int)
- (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress));
+ public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params,
+ boolean launcherOnTop) {
+ float progress = params.progress;
+ if (params.currentRect == null) {
+ RectF currentRect;
+ mTmpRectF.set(mTargetRect);
+ Utilities.scaleRectFAboutCenter(mTmpRectF, params.offsetScale);
+ currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
+ currentRect.offset(params.offsetX, 0);
+
+ // Don't clip past progress > 1.
+ progress = Math.min(1, progress);
+ final RectF sourceWindowClipInsets = params.forLiveTile
+ ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets;
+ mClipRectF.left = sourceWindowClipInsets.left * progress;
+ mClipRectF.top = sourceWindowClipInsets.top * progress;
+ mClipRectF.right =
+ mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress);
+ mClipRectF.bottom =
+ mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress);
+ params.setCurrentRectAndTargetAlpha(currentRect, 1);
+ }
- SurfaceParams[] params = new SurfaceParams[targetSet.unfilteredApps.length];
+ SurfaceParams[] surfaceParams = new SurfaceParams[targetSet.unfilteredApps.length];
for (int i = 0; i < targetSet.unfilteredApps.length; i++) {
RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i];
mTmpMatrix.setTranslate(app.position.x, app.position.y);
- Rect crop = app.sourceContainerBounds;
+ Rect crop = mTmpRect;
+ crop.set(app.sourceContainerBounds);
+ crop.offsetTo(0, 0);
float alpha = 1f;
+ int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
+ float cornerRadius = 0f;
+ float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width();
if (app.mode == targetSet.targetMode) {
+ alpha = mTaskAlphaCallback.apply(app, params.targetAlpha);
if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
- mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL);
+ mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
mTmpMatrix.postTranslate(app.position.x, app.position.y);
- crop = mClipRect;
+ mClipRectF.roundOut(crop);
+ if (mSupportsRoundedCornersOnWindows) {
+ if (params.cornerRadius > -1) {
+ cornerRadius = params.cornerRadius;
+ scale = params.currentRect.width() / crop.width();
+ } else {
+ float windowCornerRadius = mUseRoundedCornersOnWindows
+ ? mWindowCornerRadius : 0;
+ cornerRadius = Utilities.mapRange(progress, windowCornerRadius,
+ mTaskCornerRadius);
+ }
+ mCurrentCornerRadius = cornerRadius;
+ }
+ } else if (targetSet.hasRecents) {
+ // If home has a different target then recents, reverse anim the
+ // home target.
+ alpha = 1 - (progress * params.targetAlpha);
}
-
- if (app.isNotInRecents
- || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
- alpha = 1 - progress;
- }
-
- alpha = mTaskAlphaCallback.apply(app, alpha);
+ } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) {
+ crop = null;
+ layer = Integer.MAX_VALUE;
}
- params[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop,
- RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers));
+ // Since radius is in Surface space, but we draw the rounded corners in screen space, we
+ // have to undo the scale.
+ surfaceParams[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, layer,
+ cornerRadius / scale);
}
- applyParams(syncTransactionApplier, params);
- return currentRect;
+ applySurfaceParams(params.syncTransactionApplier, surfaceParams);
+ return params.currentRect;
}
- private void applyParams(@Nullable SyncRtSurfaceTransactionApplier syncTransactionApplier,
- SurfaceParams[] params) {
+ public RectF getCurrentRectWithInsets() {
+ mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF);
+ return mCurrentRectWithInsets;
+ }
+
+ private void applySurfaceParams(@Nullable SyncRtSurfaceTransactionApplierCompat
+ syncTransactionApplier, SurfaceParams[] params) {
if (syncTransactionApplier != null) {
syncTransactionApplier.scheduleApply(params);
} else {
TransactionCompat t = new TransactionCompat();
for (SurfaceParams param : params) {
- SyncRtSurfaceTransactionApplier.applyParams(t, param);
+ SyncRtSurfaceTransactionApplierCompat.applyParams(t, param);
}
t.setEarlyWakeup();
t.apply();
@@ -210,16 +252,6 @@ public class ClipAnimationHelper {
mTaskAlphaCallback = callback;
}
- public void offsetTarget(float scale, float offsetX, float offsetY, Interpolator interpolator) {
- synchronized (mTargetOffset) {
- mTargetOffset.set(offsetX, offsetY);
- }
- mTargetScale = scale;
- mInterpolator = interpolator;
- mOffsetYInterpolator = Interpolators.clampToProgress(mInterpolator, 0,
- QUICK_SCRUB_TRANSLATION_Y_FACTOR);
- }
-
public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
fromTaskThumbnailView(ttv, rv, null);
}
@@ -240,11 +272,12 @@ public class ClipAnimationHelper {
updateStackBoundsToMultiWindowTaskSize(activity);
} else {
mSourceStackBounds.set(mHomeStackBounds);
- mSourceInsets.set(activity.getDeviceProfile().getInsets());
+ Rect fallback = dl.getInsets();
+ mSourceInsets.set(ttv.getInsets(fallback));
}
- TransformedRect targetRect = new TransformedRect();
- dl.getDescendantRectRelativeToSelf(ttv, targetRect.rect);
+ Rect targetRect = new Rect();
+ dl.getDescendantRectRelativeToSelf(ttv, targetRect);
updateTargetRect(targetRect);
if (target == null) {
@@ -259,8 +292,30 @@ public class ClipAnimationHelper {
}
}
+ /**
+ * Compute scale and translation y such that the specified task view fills the screen.
+ */
+ public ClipAnimationHelper updateForFullscreenOverview(TaskView v) {
+ TaskThumbnailView thumbnailView = v.getThumbnail();
+ RecentsView recentsView = v.getRecentsView();
+ fromTaskThumbnailView(thumbnailView, recentsView);
+ Rect taskSize = new Rect();
+ recentsView.getTaskSize(taskSize);
+ updateTargetRect(taskSize);
+ return this;
+ }
+
+ /**
+ * @return The source rect's scale and translation relative to the target rect.
+ */
+ public LauncherState.ScaleAndTranslation getScaleAndTranslation() {
+ float scale = mSourceRect.width() / mTargetRect.width();
+ float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY();
+ return new LauncherState.ScaleAndTranslation(scale, 0, translationY);
+ }
+
private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
- ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy();
+ ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
if (sysUiProxy != null) {
try {
mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds());
@@ -294,29 +349,72 @@ public class ClipAnimationHelper {
mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight);
}
- public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) {
- RectF currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect);
- canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left,
- mSourceStackBounds.top - mHomeStackBounds.top);
- mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL);
-
- canvas.concat(mTmpMatrix);
- canvas.translate(mTargetRect.left, mTargetRect.top);
-
- float insetProgress = (1 - progress);
- ttv.drawOnCanvas(canvas,
- -mSourceWindowClipInsets.left * insetProgress,
- -mSourceWindowClipInsets.top * insetProgress,
- ttv.getMeasuredWidth() + mSourceWindowClipInsets.right * insetProgress,
- ttv.getMeasuredHeight() + mSourceWindowClipInsets.bottom * insetProgress,
- ttv.getCornerRadius() * progress);
- }
-
public RectF getTargetRect() {
return mTargetRect;
}
- public RectF getSourceRect() {
- return mSourceRect;
+ public float getCurrentCornerRadius() {
+ return mCurrentCornerRadius;
+ }
+
+ public static class TransformParams {
+ float progress;
+ public float offsetX;
+ public float offsetScale;
+ @Nullable RectF currentRect;
+ float targetAlpha;
+ boolean forLiveTile;
+ float cornerRadius;
+
+ SyncRtSurfaceTransactionApplierCompat syncTransactionApplier;
+
+ public TransformParams() {
+ progress = 0;
+ offsetX = 0;
+ offsetScale = 1;
+ currentRect = null;
+ targetAlpha = 0;
+ forLiveTile = false;
+ cornerRadius = -1;
+ }
+
+ public TransformParams setProgress(float progress) {
+ this.progress = progress;
+ this.currentRect = null;
+ return this;
+ }
+
+ public TransformParams setCornerRadius(float cornerRadius) {
+ this.cornerRadius = cornerRadius;
+ return this;
+ }
+
+ public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) {
+ this.currentRect = currentRect;
+ this.targetAlpha = targetAlpha;
+ return this;
+ }
+
+ public TransformParams setOffsetX(float offsetX) {
+ this.offsetX = offsetX;
+ return this;
+ }
+
+ public TransformParams setOffsetScale(float offsetScale) {
+ this.offsetScale = offsetScale;
+ return this;
+ }
+
+ public TransformParams setForLiveTile(boolean forLiveTile) {
+ this.forLiveTile = forLiveTile;
+ return this;
+ }
+
+ public TransformParams setSyncTransactionApplier(
+ SyncRtSurfaceTransactionApplierCompat applier) {
+ this.syncTransactionApplier = applier;
+ return this;
+ }
}
}
+
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ce341d8cfd7989b6be93b58dd94de2436843db5
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
@@ -0,0 +1,55 @@
+/*
+ * 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.launcher3.uioverrides.RecentsUiFactory.ROTATION_LANDSCAPE;
+import static com.android.launcher3.uioverrides.RecentsUiFactory.ROTATION_SEASCAPE;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+
+import android.content.Context;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import com.android.launcher3.graphics.RotationMode;
+import com.android.quickstep.SysUINavigationMode;
+
+/**
+ * Utility class to check nav bar position
+ */
+public class NavBarPosition {
+
+ private final SysUINavigationMode.Mode mMode;
+ private final int mDisplayRotation;
+
+ public NavBarPosition(Context context) {
+ mMode = SysUINavigationMode.getMode(context);
+ mDisplayRotation = context.getSystemService(WindowManager.class)
+ .getDefaultDisplay().getRotation();
+ }
+
+ public boolean isRightEdge() {
+ return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90;
+ }
+
+ public boolean isLeftEdge() {
+ return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_270;
+ }
+
+ public RotationMode getRotationMode() {
+ return isLeftEdge() ? ROTATION_SEASCAPE
+ : (isRightEdge() ? ROTATION_LANDSCAPE : RotationMode.NORMAL);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..83601e6175a29ad27d2c3d8a3708458bc3170689
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
@@ -0,0 +1,114 @@
+/*
+ * 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.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+
+import android.graphics.Rect;
+import android.util.ArraySet;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.RecentsAnimationListener;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.Set;
+import java.util.function.Consumer;
+
+import androidx.annotation.UiThread;
+
+/**
+ * Wrapper around {@link RecentsAnimationListener} which delegates callbacks to multiple listeners
+ * on the main thread
+ */
+public class RecentsAnimationListenerSet implements RecentsAnimationListener {
+
+ // The actual app surface is replaced by a screenshot upon recents animation cancelation when
+ // deferredWithScreenshot is true. Launcher takes the responsibility to clean up this screenshot
+ // after app transition is finished. This delay is introduced to cover the app transition
+ // period of time.
+ private final int TRANSITION_DELAY = 100;
+
+ private final Set mListeners = new ArraySet<>();
+ private final boolean mShouldMinimizeSplitScreen;
+ private final Consumer mOnFinishListener;
+ private RecentsAnimationControllerCompat mController;
+
+ private boolean mCancelled;
+
+ public RecentsAnimationListenerSet(boolean shouldMinimizeSplitScreen,
+ Consumer onFinishListener) {
+ mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
+ mOnFinishListener = onFinishListener;
+ }
+
+ @UiThread
+ public void addListener(SwipeAnimationListener listener) {
+ Preconditions.assertUIThread();
+ mListeners.add(listener);
+ }
+
+ @UiThread
+ public void removeListener(SwipeAnimationListener listener) {
+ Preconditions.assertUIThread();
+ mListeners.remove(listener);
+ }
+
+ @Override
+ public final void onAnimationStart(RecentsAnimationControllerCompat controller,
+ RemoteAnimationTargetCompat[] targets, Rect homeContentInsets,
+ Rect minimizedHomeBounds) {
+ mController = controller;
+ SwipeAnimationTargetSet targetSet = new SwipeAnimationTargetSet(controller, targets,
+ homeContentInsets, minimizedHomeBounds, mShouldMinimizeSplitScreen,
+ mOnFinishListener);
+
+ if (mCancelled) {
+ targetSet.cancelAnimation();
+ } else {
+ Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(), () -> {
+ for (SwipeAnimationListener listener : getListeners()) {
+ listener.onRecentsAnimationStart(targetSet);
+ }
+ });
+ }
+ }
+
+ @Override
+ public final void onAnimationCanceled(boolean deferredWithScreenshot) {
+ Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(), () -> {
+ for (SwipeAnimationListener listener : getListeners()) {
+ listener.onRecentsAnimationCanceled();
+ }
+ });
+ // TODO: handle the transition better instead of simply using a transition delay.
+ if (deferredWithScreenshot) {
+ MAIN_THREAD_EXECUTOR.getHandler().postDelayed(() -> mController.cleanupScreenshot(),
+ TRANSITION_DELAY);
+ }
+ }
+
+ private SwipeAnimationListener[] getListeners() {
+ return mListeners.toArray(new SwipeAnimationListener[mListeners.size()]);
+ }
+
+ public void cancelListener() {
+ mCancelled = true;
+ onAnimationCanceled(false);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
new file mode 100644
index 0000000000000000000000000000000000000000..77dc6f32e7be069e8f528822d06e63fb3ef0dac3
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -0,0 +1,231 @@
+/*
+ * 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 android.animation.Animator;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.FlingSpringAnim;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Applies spring forces to animate from a starting rect to a target rect,
+ * while providing update callbacks to the caller.
+ */
+public class RectFSpringAnim {
+
+ private static final FloatPropertyCompat RECT_CENTER_X =
+ new FloatPropertyCompat("rectCenterXSpring") {
+ @Override
+ public float getValue(RectFSpringAnim anim) {
+ return anim.mCurrentCenterX;
+ }
+
+ @Override
+ public void setValue(RectFSpringAnim anim, float currentCenterX) {
+ anim.mCurrentCenterX = currentCenterX;
+ anim.onUpdate();
+ }
+ };
+
+ private static final FloatPropertyCompat RECT_Y =
+ new FloatPropertyCompat("rectYSpring") {
+ @Override
+ public float getValue(RectFSpringAnim anim) {
+ return anim.mCurrentY;
+ }
+
+ @Override
+ public void setValue(RectFSpringAnim anim, float y) {
+ anim.mCurrentY = y;
+ anim.onUpdate();
+ }
+ };
+
+ private static final FloatPropertyCompat RECT_SCALE_PROGRESS =
+ new FloatPropertyCompat("rectScaleProgress") {
+ @Override
+ public float getValue(RectFSpringAnim object) {
+ return object.mCurrentScaleProgress;
+ }
+
+ @Override
+ public void setValue(RectFSpringAnim object, float value) {
+ object.mCurrentScaleProgress = value;
+ object.onUpdate();
+ }
+ };
+
+ private final RectF mStartRect;
+ private final RectF mTargetRect;
+ private final RectF mCurrentRect = new RectF();
+ private final List mOnUpdateListeners = new ArrayList<>();
+ private final List mAnimatorListeners = new ArrayList<>();
+
+ private float mCurrentCenterX;
+ private float mCurrentY;
+ // If true, tracking the bottom of the rects, else tracking the top.
+ private boolean mTrackingBottomY;
+ private float mCurrentScaleProgress;
+ private FlingSpringAnim mRectXAnim;
+ private FlingSpringAnim mRectYAnim;
+ private SpringAnimation mRectScaleAnim;
+ private boolean mAnimsStarted;
+ private boolean mRectXAnimEnded;
+ private boolean mRectYAnimEnded;
+ private boolean mRectScaleAnimEnded;
+
+ private float mMinVisChange;
+ private float mYOvershoot;
+
+ public RectFSpringAnim(RectF startRect, RectF targetRect, Resources resources) {
+ mStartRect = startRect;
+ mTargetRect = targetRect;
+ mCurrentCenterX = mStartRect.centerX();
+
+ mTrackingBottomY = startRect.bottom < targetRect.bottom;
+ mCurrentY = mTrackingBottomY ? mStartRect.bottom : mStartRect.top;
+
+ mMinVisChange = resources.getDimensionPixelSize(R.dimen.swipe_up_fling_min_visible_change);
+ mYOvershoot = resources.getDimensionPixelSize(R.dimen.swipe_up_y_overshoot);
+ }
+
+ public void onTargetPositionChanged() {
+ if (mRectXAnim != null && mRectXAnim.getTargetPosition() != mTargetRect.centerX()) {
+ mRectXAnim.updatePosition(mCurrentCenterX, mTargetRect.centerX());
+ }
+
+ if (mRectYAnim != null) {
+ if (mTrackingBottomY && mRectYAnim.getTargetPosition() != mTargetRect.bottom) {
+ mRectYAnim.updatePosition(mCurrentY, mTargetRect.bottom);
+ } else if (!mTrackingBottomY && mRectYAnim.getTargetPosition() != mTargetRect.top) {
+ mRectYAnim.updatePosition(mCurrentY, mTargetRect.top);
+ }
+ }
+ }
+
+ public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
+ mOnUpdateListeners.add(onUpdateListener);
+ }
+
+ public void addAnimatorListener(Animator.AnimatorListener animatorListener) {
+ mAnimatorListeners.add(animatorListener);
+ }
+
+ public void start(PointF velocityPxPerMs) {
+ // Only tell caller that we ended if both x and y animations have ended.
+ OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> {
+ mRectXAnimEnded = true;
+ maybeOnEnd();
+ });
+ OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> {
+ mRectYAnimEnded = true;
+ maybeOnEnd();
+ });
+
+ float startX = mCurrentCenterX;
+ float endX = mTargetRect.centerX();
+ float minXValue = Math.min(startX, endX);
+ float maxXValue = Math.max(startX, endX);
+ mRectXAnim = new FlingSpringAnim(this, RECT_CENTER_X, startX, endX,
+ velocityPxPerMs.x * 1000, mMinVisChange, minXValue, maxXValue, 1f, onXEndListener);
+
+ float startVelocityY = velocityPxPerMs.y * 1000;
+ // Scale the Y velocity based on the initial velocity to tune the curves.
+ float springVelocityFactor = 0.1f + 0.9f * Math.abs(startVelocityY) / 20000.0f;
+ float startY = mCurrentY;
+ float endY = mTrackingBottomY ? mTargetRect.bottom : mTargetRect.top;
+ float minYValue = Math.min(startY, endY - mYOvershoot);
+ float maxYValue = Math.max(startY, endY);
+ mRectYAnim = new FlingSpringAnim(this, RECT_Y, startY, endY, startVelocityY,
+ mMinVisChange, minYValue, maxYValue, springVelocityFactor, onYEndListener);
+
+ float minVisibleChange = 1f / mStartRect.height();
+ mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS)
+ .setSpring(new SpringForce(1f)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+ .setStiffness(SpringForce.STIFFNESS_LOW))
+ .setStartVelocity(velocityPxPerMs.y * minVisibleChange)
+ .setMaxValue(1f)
+ .setMinimumVisibleChange(minVisibleChange)
+ .addEndListener((animation, canceled, value, velocity) -> {
+ mRectScaleAnimEnded = true;
+ maybeOnEnd();
+ });
+
+ mRectXAnim.start();
+ mRectYAnim.start();
+ mRectScaleAnim.start();
+ mAnimsStarted = true;
+ for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
+ animatorListener.onAnimationStart(null);
+ }
+ }
+
+ public void end() {
+ if (mAnimsStarted) {
+ mRectXAnim.end();
+ mRectYAnim.end();
+ if (mRectScaleAnim.canSkipToEnd()) {
+ mRectScaleAnim.skipToEnd();
+ }
+ }
+ }
+
+ private void onUpdate() {
+ if (!mOnUpdateListeners.isEmpty()) {
+ float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
+ mTargetRect.width());
+ float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(),
+ mTargetRect.height());
+ if (mTrackingBottomY) {
+ mCurrentRect.set(mCurrentCenterX - currentWidth / 2, mCurrentY - currentHeight,
+ mCurrentCenterX + currentWidth / 2, mCurrentY);
+ } else {
+ mCurrentRect.set(mCurrentCenterX - currentWidth / 2, mCurrentY,
+ mCurrentCenterX + currentWidth / 2, mCurrentY + currentHeight);
+ }
+ for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
+ onUpdateListener.onUpdate(mCurrentRect, mCurrentScaleProgress);
+ }
+ }
+ }
+
+ private void maybeOnEnd() {
+ if (mAnimsStarted && mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded) {
+ mAnimsStarted = false;
+ for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
+ animatorListener.onAnimationEnd(null);
+ }
+ }
+ }
+
+ public interface OnUpdateListener {
+ void onUpdate(RectF currentRect, float progress);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
new file mode 100644
index 0000000000000000000000000000000000000000..07e96869ed5f9ef2fe70d0f3a087979453ac3bad
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -0,0 +1,169 @@
+/*
+ * 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 android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager.AnimationConfig;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.anim.SpringObjectAnimator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+/**
+ * Creates an animation where all the workspace items are moved into their final location,
+ * staggered row by row from the bottom up.
+ * This is used in conjunction with the swipe up to home animation.
+ */
+public class StaggeredWorkspaceAnim {
+
+ private static final int APP_CLOSE_ROW_START_DELAY_MS = 10;
+ private static final int ALPHA_DURATION_MS = 250;
+
+ private static final float MAX_VELOCITY_PX_PER_S = 22f;
+
+ private static final float DAMPING_RATIO = 0.7f;
+ private static final float STIFFNESS = 150f;
+
+ private final float mVelocity;
+ private final float mSpringTransY;
+ private final View mViewToIgnore;
+
+ private final List mAnimators = new ArrayList<>();
+
+ /**
+ * @param floatingViewOriginalView The FloatingIconView's original view.
+ */
+ public StaggeredWorkspaceAnim(Launcher launcher, @Nullable View floatingViewOriginalView,
+ float velocity) {
+ mVelocity = velocity;
+ // We ignore this view since it's visibility and position is controlled by
+ // the FloatingIconView.
+ mViewToIgnore = floatingViewOriginalView;
+
+ // Scale the translationY based on the initial velocity to better sync the workspace items
+ // with the floating view.
+ float transFactor = 0.2f + 0.9f * Math.abs(velocity) / MAX_VELOCITY_PX_PER_S;
+ mSpringTransY = transFactor * launcher.getResources()
+ .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y);
+
+ DeviceProfile grid = launcher.getDeviceProfile();
+ ShortcutAndWidgetContainer currentPage = ((CellLayout) launcher.getWorkspace()
+ .getChildAt(launcher.getWorkspace().getCurrentPage()))
+ .getShortcutsAndWidgets();
+
+ // Hotseat and QSB takes up two additional rows.
+ int totalRows = grid.inv.numRows + (grid.isVerticalBarLayout() ? 0 : 2);
+
+ // Set up springs on workspace items.
+ for (int i = currentPage.getChildCount() - 1; i >= 0; i--) {
+ View child = currentPage.getChildAt(i);
+ CellLayout.LayoutParams lp = ((CellLayout.LayoutParams) child.getLayoutParams());
+ addStaggeredAnimationForView(child, lp.cellY + lp.cellVSpan, totalRows);
+ }
+
+ // Set up springs for the hotseat and qsb.
+ if (grid.isVerticalBarLayout()) {
+ ViewGroup hotseat = (ViewGroup) launcher.getHotseat().getChildAt(0);
+ for (int i = hotseat.getChildCount() - 1; i >= 0; i--) {
+ View child = hotseat.getChildAt(i);
+ CellLayout.LayoutParams lp = ((CellLayout.LayoutParams) child.getLayoutParams());
+ addStaggeredAnimationForView(child, lp.cellY + 1, totalRows);
+ }
+ } else {
+ View hotseat = launcher.getHotseat().getChildAt(0);
+ addStaggeredAnimationForView(hotseat, grid.inv.numRows + 1, totalRows);
+
+ View qsb = launcher.findViewById(R.id.search_container_all_apps);
+ addStaggeredAnimationForView(qsb, grid.inv.numRows + 2, totalRows);
+ }
+
+ addWorkspaceScrimAnimationForState(launcher, BACKGROUND_APP, 0);
+ addWorkspaceScrimAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
+ }
+
+ /**
+ * Starts the animation.
+ */
+ public void start() {
+ for (Animator a : mAnimators) {
+ if (a instanceof SpringObjectAnimator) {
+ ((SpringObjectAnimator) a).startSpring(1f, mVelocity, null);
+ } else {
+ a.start();
+ }
+ }
+ }
+
+ /**
+ * Adds an alpha/trans animator for {@param v}, with a start delay based on the view's row.
+ *
+ * @param v A view on the workspace.
+ * @param row The bottom-most row that contains the view.
+ * @param totalRows Total number of rows.
+ */
+ private void addStaggeredAnimationForView(View v, int row, int totalRows) {
+ if (v == mViewToIgnore) {
+ return;
+ }
+
+ // Invert the rows, because we stagger starting from the bottom of the screen.
+ int invertedRow = totalRows - row;
+ // Add 1 to the inverted row so that the bottom most row has a start delay.
+ long startDelay = (long) ((invertedRow + 1) * APP_CLOSE_ROW_START_DELAY_MS);
+
+ v.setTranslationY(mSpringTransY);
+ SpringObjectAnimator springTransY = new SpringObjectAnimator<>(v, VIEW_TRANSLATE_Y,
+ 1f, DAMPING_RATIO, STIFFNESS, mSpringTransY, 0);
+ springTransY.setStartDelay(startDelay);
+ mAnimators.add(springTransY);
+
+ v.setAlpha(0);
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
+ alpha.setInterpolator(LINEAR);
+ alpha.setDuration(ALPHA_DURATION_MS);
+ alpha.setStartDelay(startDelay);
+ mAnimators.add(alpha);
+ }
+
+ private void addWorkspaceScrimAnimationForState(Launcher launcher, LauncherState state,
+ long duration) {
+ AnimatorSetBuilder scrimAnimBuilder = new AnimatorSetBuilder();
+ AnimationConfig scrimAnimConfig = new AnimationConfig();
+ scrimAnimConfig.duration = duration;
+ PropertySetter scrimPropertySetter = scrimAnimConfig.getPropertySetter(scrimAnimBuilder);
+ launcher.getWorkspace().getStateTransitionAnimation().setScrim(scrimPropertySetter, state);
+ mAnimators.add(scrimAnimBuilder.build());
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..381c27a2849672b87cb8694694ccb2c578cdfc6c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
@@ -0,0 +1,119 @@
+/*
+ * 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.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
+import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+
+import android.graphics.Rect;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.Consumer;
+
+/**
+ * Extension of {@link RemoteAnimationTargetSet} with additional information about swipe
+ * up animation
+ */
+public class SwipeAnimationTargetSet extends RemoteAnimationTargetSet {
+
+ private final boolean mShouldMinimizeSplitScreen;
+ private final Consumer mOnFinishListener;
+
+ public final RecentsAnimationControllerCompat controller;
+ public final Rect homeContentInsets;
+ public final Rect minimizedHomeBounds;
+
+ public SwipeAnimationTargetSet(RecentsAnimationControllerCompat controller,
+ RemoteAnimationTargetCompat[] targets, Rect homeContentInsets,
+ Rect minimizedHomeBounds, boolean shouldMinimizeSplitScreen,
+ Consumer onFinishListener) {
+ super(targets, MODE_CLOSING);
+ this.controller = controller;
+ this.homeContentInsets = homeContentInsets;
+ this.minimizedHomeBounds = minimizedHomeBounds;
+ this.mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
+ this.mOnFinishListener = onFinishListener;
+ }
+
+ public boolean hasTargets() {
+ return unfilteredApps.length != 0;
+ }
+
+ /**
+ * Clones the target set without any actual targets. Used only when continuing a gesture after
+ * the actual recents animation has finished.
+ */
+ public SwipeAnimationTargetSet cloneWithoutTargets() {
+ return new SwipeAnimationTargetSet(controller, new RemoteAnimationTargetCompat[0],
+ homeContentInsets, minimizedHomeBounds, mShouldMinimizeSplitScreen,
+ mOnFinishListener);
+ }
+
+ public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
+ mOnFinishListener.accept(this);
+ BACKGROUND_EXECUTOR.execute(() -> {
+ controller.setInputConsumerEnabled(false);
+ controller.finish(toRecents, sendUserLeaveHint);
+
+ if (callback != null) {
+ MAIN_THREAD_EXECUTOR.execute(callback);
+ }
+ });
+ }
+
+ public void enableInputConsumer() {
+ BACKGROUND_EXECUTOR.submit(() -> {
+ controller.hideCurrentInputMethod();
+ controller.setInputConsumerEnabled(true);
+ });
+ }
+
+ public void setWindowThresholdCrossed(boolean thresholdCrossed) {
+ BACKGROUND_EXECUTOR.execute(() -> {
+ controller.setAnimationTargetsBehindSystemBars(!thresholdCrossed);
+ if (mShouldMinimizeSplitScreen && thresholdCrossed) {
+ // NOTE: As a workaround for conflicting animations (Launcher animating the task
+ // leash, and SystemUI resizing the docked stack, which resizes the task), we
+ // currently only set the minimized mode, and not the inverse.
+ // TODO: Synchronize the minimize animation with the launcher animation
+ controller.setSplitScreenMinimized(thresholdCrossed);
+ }
+ });
+ }
+
+ public ThumbnailData screenshotTask(int taskId) {
+ return controller != null ? controller.screenshotTask(taskId) : null;
+ }
+
+ public void cancelAnimation() {
+ finishController(false /* toRecents */, null, false /* sendUserLeaveHint */);
+ }
+
+ public void finishAnimation() {
+ finishController(true /* toRecents */, null, false /* sendUserLeaveHint */);
+ }
+
+ public interface SwipeAnimationListener {
+
+ void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet);
+
+ void onRecentsAnimationCanceled();
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ddf1b60f37cc0bad2c32623ab2b46d868456876
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
+
+import android.content.Context;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+public class TaskCornerRadius {
+
+ public static float get(Context context) {
+ return supportsRoundedCornersOnWindows(context.getResources()) ?
+ Themes.getDialogCornerRadius(context):
+ context.getResources().getDimension(R.dimen.task_corner_radius_small);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
similarity index 73%
rename from quickstep/src/com/android/quickstep/views/ClearAllButton.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
index fbecd8486ae172a5b4742a54ad0a2ec2d8365110..9db0c09ae247a7271e7ab9a20aee65c3235fa90e 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
@@ -18,6 +18,7 @@ package com.android.quickstep.views;
import android.content.Context;
import android.util.AttributeSet;
+import android.util.Property;
import android.widget.Button;
import com.android.launcher3.Utilities;
@@ -26,8 +27,22 @@ import com.android.quickstep.views.RecentsView.ScrollState;
public class ClearAllButton extends Button implements PageCallbacks {
+ public static final Property VISIBILITY_ALPHA =
+ new Property(Float.class, "visibilityAlpha") {
+ @Override
+ public Float get(ClearAllButton view) {
+ return view.mVisibilityAlpha;
+ }
+
+ @Override
+ public void set(ClearAllButton view, Float visibilityAlpha) {
+ view.setVisibilityAlpha(visibilityAlpha);
+ }
+ };
+
private float mScrollAlpha = 1;
private float mContentAlpha = 1;
+ private float mVisibilityAlpha = 1;
private final boolean mIsRtl;
@@ -58,6 +73,13 @@ public class ClearAllButton extends Button implements PageCallbacks {
}
}
+ public void setVisibilityAlpha(float alpha) {
+ if (mVisibilityAlpha != alpha) {
+ mVisibilityAlpha = alpha;
+ updateAlpha();
+ }
+ }
+
@Override
public void onPageScroll(ScrollState scrollState) {
float width = getWidth();
@@ -72,7 +94,7 @@ public class ClearAllButton extends Button implements PageCallbacks {
}
private void updateAlpha() {
- final float alpha = mScrollAlpha * mContentAlpha;
+ final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha;
setAlpha(alpha);
setClickable(alpha == 1);
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fac813854ca7ec5d80c40698fbe6ed639dc4406
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -0,0 +1,237 @@
+/*
+ * 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.
+ */
+
+package com.android.quickstep.views;
+
+import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
+
+import static com.android.launcher3.Utilities.prefixTextWithIcon;
+
+import android.annotation.TargetApi;
+import android.app.ActivityOptions;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.AppUsageLimit;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
+import android.os.Build;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.StringRes;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.time.Duration;
+import java.util.Locale;
+
+@TargetApi(Build.VERSION_CODES.Q)
+public final class DigitalWellBeingToast {
+ static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
+ static final int MINUTE_MS = 60000;
+
+ private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
+
+ private final BaseDraggingActivity mActivity;
+ private final TaskView mTaskView;
+ private final LauncherApps mLauncherApps;
+
+ private Task mTask;
+ private boolean mHasLimit;
+ private long mAppRemainingTimeMs;
+
+ public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
+ mActivity = activity;
+ mTaskView = taskView;
+ mLauncherApps = activity.getSystemService(LauncherApps.class);
+ }
+
+ private void setTaskFooter(View view) {
+ View oldFooter = mTaskView.setFooter(TaskView.INDEX_DIGITAL_WELLBEING_TOAST, view);
+ if (oldFooter != null) {
+ oldFooter.setOnClickListener(null);
+ mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, oldFooter);
+ }
+ }
+
+ private void setNoLimit() {
+ mHasLimit = false;
+ mTaskView.setContentDescription(mTask.titleDescription);
+ setTaskFooter(null);
+ mAppRemainingTimeMs = 0;
+ }
+
+ private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
+ mAppRemainingTimeMs = appRemainingTimeMs;
+ mHasLimit = true;
+ TextView toast = mActivity.getViewCache().getView(R.layout.digital_wellbeing_toast,
+ mActivity, mTaskView);
+ toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText()));
+ toast.setOnClickListener(this::openAppUsageSettings);
+ setTaskFooter(toast);
+
+ mTaskView.setContentDescription(
+ getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
+ RecentsView rv = mTaskView.getRecentsView();
+ if (rv != null) {
+ rv.onDigitalWellbeingToastShown();
+ }
+ }
+
+ public String getText() {
+ return getText(mAppRemainingTimeMs);
+ }
+
+ public boolean hasLimit() {
+ return mHasLimit;
+ }
+
+ public void initialize(Task task) {
+ mTask = task;
+
+ if (task.key.userId != UserHandle.myUserId()) {
+ setNoLimit();
+ return;
+ }
+
+ Utilities.THREAD_POOL_EXECUTOR.execute(() -> {
+ final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
+ task.getTopComponent().getPackageName(),
+ UserHandle.of(task.key.userId));
+
+ final long appUsageLimitTimeMs =
+ usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
+ final long appRemainingTimeMs =
+ usageLimit != null ? usageLimit.getUsageRemaining() : -1;
+
+ mTaskView.post(() -> {
+ if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+ setNoLimit();
+ } else {
+ setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
+ }
+ });
+ });
+ }
+
+ private String getReadableDuration(
+ Duration duration,
+ FormatWidth formatWidthHourAndMinute,
+ @StringRes int durationLessThanOneMinuteStringId,
+ boolean forceFormatWidth) {
+ int hours = Math.toIntExact(duration.toHours());
+ int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
+
+ // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero.
+ if (hours > 0 && minutes > 0) {
+ return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute)
+ .formatMeasures(
+ new Measure(hours, MeasureUnit.HOUR),
+ new Measure(minutes, MeasureUnit.MINUTE));
+ }
+
+ // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced).
+ if (hours > 0) {
+ return MeasureFormat.getInstance(
+ Locale.getDefault(),
+ forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(hours, MeasureUnit.HOUR));
+ }
+
+ // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced).
+ if (minutes > 0) {
+ return MeasureFormat.getInstance(
+ Locale.getDefault()
+ , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE));
+ }
+
+ // Use a specific string for usage less than one minute but non-zero.
+ if (duration.compareTo(Duration.ZERO) > 0) {
+ return mActivity.getString(durationLessThanOneMinuteStringId);
+ }
+
+ // Otherwise, return 0-minute string.
+ return MeasureFormat.getInstance(
+ Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(0, MeasureUnit.MINUTE));
+ }
+
+ private String getReadableDuration(
+ Duration duration,
+ FormatWidth formatWidthHourAndMinute,
+ @StringRes int durationLessThanOneMinuteStringId) {
+ return getReadableDuration(
+ duration,
+ formatWidthHourAndMinute,
+ durationLessThanOneMinuteStringId,
+ /* forceFormatWidth= */ false);
+ }
+
+ private String getRoundedUpToMinuteReadableDuration(long remainingTime) {
+ final Duration duration = Duration.ofMillis(
+ remainingTime > MINUTE_MS ?
+ (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
+ remainingTime);
+ return getReadableDuration(
+ duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute);
+ }
+
+ private String getText(long remainingTime) {
+ return mActivity.getString(
+ R.string.time_left_for_app,
+ getRoundedUpToMinuteReadableDuration(remainingTime));
+ }
+
+ public void openAppUsageSettings(View view) {
+ final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
+ .putExtra(Intent.EXTRA_PACKAGE_NAME,
+ mTask.getTopComponent().getPackageName()).addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ try {
+ final BaseActivity activity = BaseActivity.fromContext(view.getContext());
+ final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
+ view, 0, 0,
+ view.getWidth(), view.getHeight());
+ activity.startActivity(intent, options.toBundle());
+ activity.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
+ LauncherLogProto.ControlType.APP_USAGE_SETTINGS, view);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Failed to open app usage settings for task "
+ + mTask.getTopComponent().getPackageName(), e);
+ }
+ }
+
+ private String getContentDescriptionForTask(
+ Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
+ return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
+ mActivity.getString(
+ R.string.task_contents_description_with_remaining_time,
+ task.titleDescription,
+ getText(appRemainingTimeMs)) :
+ task.titleDescription;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
similarity index 66%
rename from quickstep/src/com/android/quickstep/views/IconView.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
index c359966df41f34b1792e84d38ed6555fc38ad51e..eb8da6e58b637f2afb2f790d216b2469fda63679 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
@@ -21,14 +21,26 @@ import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
+import com.android.launcher3.FastBitmapDrawable;
+
+import java.util.ArrayList;
+
+import androidx.annotation.NonNull;
+
/**
* A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
* when the drawable changes.
*/
public class IconView extends View {
+ public interface OnScaleUpdateListener {
+ public void onScaleUpdate(float scale);
+ }
+
private Drawable mDrawable;
+ private ArrayList mScaleListeners;
+
public IconView(Context context) {
super(context);
}
@@ -53,6 +65,10 @@ public class IconView extends View {
invalidate();
}
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
@@ -77,6 +93,16 @@ public class IconView extends View {
}
}
+ @Override
+ public void invalidateDrawable(@NonNull Drawable drawable) {
+ super.invalidateDrawable(drawable);
+ if (drawable instanceof FastBitmapDrawable && mScaleListeners != null) {
+ for (OnScaleUpdateListener listener : mScaleListeners) {
+ listener.onScaleUpdate(((FastBitmapDrawable) drawable).getScale());
+ }
+ }
+ }
+
@Override
protected void onDraw(Canvas canvas) {
if (mDrawable != null) {
@@ -88,4 +114,20 @@ public class IconView extends View {
public boolean hasOverlappingRendering() {
return false;
}
+
+ public void addUpdateScaleListener(OnScaleUpdateListener listener) {
+ if (mScaleListeners == null) {
+ mScaleListeners = new ArrayList<>();
+ }
+ mScaleListeners.add(listener);
+ if (mDrawable instanceof FastBitmapDrawable) {
+ listener.onScaleUpdate(((FastBitmapDrawable) mDrawable).getScale());
+ }
+ }
+
+ public void removeUpdateScaleListener(OnScaleUpdateListener listener) {
+ if (mScaleListeners != null) {
+ mScaleListeners.remove(listener);
+ }
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
new file mode 100644
index 0000000000000000000000000000000000000000..03441c87ecfbfe4dc4a3695af3ceda1d40ffc120
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -0,0 +1,267 @@
+/*
+ * 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.
+ */
+package com.android.quickstep.views;
+
+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.LauncherState.SPRING_LOADED;
+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_QUICKSTEP_LIVE_TILE;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager.StateListener;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.Interpolators;
+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.util.ClipAnimationHelper;
+import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
+import com.android.quickstep.util.LayoutUtils;
+
+/**
+ * {@link RecentsView} used in Launcher activity
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class LauncherRecentsView extends RecentsView implements StateListener {
+
+ private final TransformParams mTransformParams = new TransformParams();
+
+ public LauncherRecentsView(Context context) {
+ this(context, null);
+ }
+
+ public LauncherRecentsView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public LauncherRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setContentAlpha(0);
+ mActivity.getStateManager().addStateListener(this);
+ }
+
+ @Override
+ public void startHome() {
+ mActivity.getStateManager().goToState(NORMAL);
+ }
+
+ @Override
+ public void setTranslationY(float translationY) {
+ super.setTranslationY(translationY);
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ LauncherState state = mActivity.getStateManager().getState();
+ if (state == OVERVIEW || state == ALL_APPS) {
+ redrawLiveTile(false);
+ }
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ maybeDrawEmptyMessage(canvas);
+ super.draw(canvas);
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ updateEmptyMessage();
+ }
+
+ @Override
+ protected void onTaskStackUpdated() {
+ // Lazily update the empty message only when the task stack is reapplied
+ updateEmptyMessage();
+ }
+
+ /**
+ * Animates adjacent tasks and translate hotseat off screen as well.
+ */
+ @Override
+ public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv,
+ ClipAnimationHelper helper) {
+ AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv, helper);
+
+ if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
+ // Hotseat doesn't move when opening recents with the button,
+ // so don't animate it here either.
+ return anim;
+ }
+
+ float allAppsProgressOffscreen = ALL_APPS_PROGRESS_OFF_SCREEN;
+ LauncherState state = mActivity.getStateManager().getState();
+ if ((state.getVisibleElements(mActivity) & ALL_APPS_HEADER_EXTRA) != 0) {
+ float maxShiftRange = mActivity.getDeviceProfile().heightPx;
+ float currShiftRange = mActivity.getAllAppsController().getShiftRange();
+ allAppsProgressOffscreen = 1f + (maxShiftRange - currShiftRange) / maxShiftRange;
+ }
+ anim.play(ObjectAnimator.ofFloat(
+ mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen));
+
+ ObjectAnimator dragHandleAnim = ObjectAnimator.ofInt(
+ mActivity.findViewById(R.id.scrim_view), ScrimView.DRAG_HANDLE_ALPHA, 0);
+ dragHandleAnim.setInterpolator(Interpolators.ACCEL_2);
+ anim.play(dragHandleAnim);
+
+ return anim;
+ }
+
+ @Override
+ protected void getTaskSize(DeviceProfile dp, Rect outRect) {
+ LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
+ }
+
+ @Override
+ protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (mRecentsAnimationWrapper.targetSet != null && tv.isRunningTask()) {
+ mTransformParams.setProgress(1 - progress)
+ .setSyncTransactionApplier(mSyncTransactionApplier)
+ .setForLiveTile(true);
+ mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
+ mTransformParams);
+ } else {
+ redrawLiveTile(true);
+ }
+ }
+ }
+
+ @Override
+ protected void onTaskLaunched(boolean success) {
+ if (success) {
+ mActivity.getStateManager().goToState(NORMAL, false /* animate */);
+ } else {
+ LauncherState state = mActivity.getStateManager().getState();
+ mActivity.getAllAppsController().setState(state);
+ }
+ super.onTaskLaunched(success);
+ }
+
+ @Override
+ public boolean shouldUseMultiWindowTaskSizeStrategy() {
+ return mActivity.isInMultiWindowMode();
+ }
+
+ @Override
+ public void scrollTo(int x, int y) {
+ super.scrollTo(x, y);
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile) {
+ redrawLiveTile(true);
+ }
+ }
+
+ @Override
+ public void redrawLiveTile(boolean mightNeedToRefill) {
+ if (!mEnableDrawingLiveTile || mRecentsAnimationWrapper == null
+ || mClipAnimationHelper == null) {
+ return;
+ }
+ TaskView taskView = getRunningTaskView();
+ if (taskView != null) {
+ taskView.getThumbnail().getGlobalVisibleRect(mTempRect);
+ int offsetX = (int) (mTaskWidth * taskView.getScaleX() * getScaleX()
+ - mTempRect.width());
+ int offsetY = (int) (mTaskHeight * taskView.getScaleY() * getScaleY()
+ - mTempRect.height());
+ if (((mCurrentPage != 0) || mightNeedToRefill) && offsetX > 0) {
+ if (mTempRect.left - offsetX < 0) {
+ mTempRect.left -= offsetX;
+ } else {
+ mTempRect.right += offsetX;
+ }
+ }
+ if (mightNeedToRefill && offsetY > 0) {
+ mTempRect.top -= offsetY;
+ }
+ mTempRectF.set(mTempRect);
+ mTransformParams.setProgress(1f)
+ .setCurrentRectAndTargetAlpha(mTempRectF, taskView.getAlpha())
+ .setSyncTransactionApplier(mSyncTransactionApplier);
+ if (mRecentsAnimationWrapper.targetSet != null) {
+ mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
+ mTransformParams);
+ }
+ }
+ }
+
+ @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 onStateTransitionStart(LauncherState toState) {
+ setOverviewStateEnabled(toState.overviewUi);
+ setFreezeViewVisibility(true);
+ }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (finalState == NORMAL || finalState == SPRING_LOADED) {
+ // Clean-up logic that occurs when recents is no longer in use/visible.
+ reset();
+ }
+ setOverlayEnabled(finalState == OVERVIEW);
+ setFreezeViewVisibility(false);
+ }
+
+ @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);
+ }
+ }
+
+ @Override
+ protected boolean shouldStealTouchFromSiblingsBelow(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ // Allow touches to go through to the hotseat.
+ Hotseat hotseat = mActivity.getHotseat();
+ boolean touchingHotseat = hotseat.isShown()
+ && mActivity.getDragLayer().isEventOverView(hotseat, ev, this);
+ return !touchingHotseat;
+ }
+ return super.shouldStealTouchFromSiblingsBelow(ev);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
new file mode 100644
index 0000000000000000000000000000000000000000..a83879738ddef08d494d1b74d3b09609341d79e0
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
@@ -0,0 +1,131 @@
+package com.android.quickstep.views;
+
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.FloatProperty;
+
+import com.android.launcher3.anim.Interpolators;
+
+public class LiveTileOverlay extends Drawable {
+
+ private static final long ICON_ANIM_DURATION = 120;
+
+ private static final FloatProperty PROGRESS =
+ new FloatProperty("progress") {
+ @Override
+ public void setValue(LiveTileOverlay liveTileOverlay, float progress) {
+ liveTileOverlay.setIconAnimationProgress(progress);
+ }
+
+ @Override
+ public Float get(LiveTileOverlay liveTileOverlay) {
+ return liveTileOverlay.mIconAnimationProgress;
+ }
+ };
+
+ private final Paint mPaint = new Paint();
+
+ private Rect mBoundsRect = new Rect();
+ private RectF mCurrentRect;
+ private float mCornerRadius;
+ private Drawable mIcon;
+ private Animator mIconAnimator;
+
+ private boolean mDrawEnabled = true;
+ private float mIconAnimationProgress = 0f;
+
+ public LiveTileOverlay() {
+ mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ }
+
+ public void update(RectF currentRect, float cornerRadius) {
+ invalidateSelf();
+
+ mCurrentRect = currentRect;
+ mCornerRadius = cornerRadius;
+
+ mCurrentRect.roundOut(mBoundsRect);
+ setBounds(mBoundsRect);
+ invalidateSelf();
+ }
+
+ public void setIcon(Drawable icon) {
+ mIcon = icon;
+ }
+
+ public void startIconAnimation() {
+ if (mIconAnimator != null) {
+ mIconAnimator.cancel();
+ }
+ // This animator must match the icon part of {@link TaskView#FOCUS_TRANSITION} animation.
+ mIconAnimator = ObjectAnimator.ofFloat(this, PROGRESS, 1);
+ mIconAnimator.setDuration(ICON_ANIM_DURATION).setInterpolator(LINEAR);
+ mIconAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIconAnimator = null;
+ }
+ });
+ mIconAnimator.start();
+ }
+
+ public float cancelIconAnimation() {
+ if (mIconAnimator != null) {
+ mIconAnimator.cancel();
+ }
+ return mIconAnimationProgress;
+ }
+
+ public void setDrawEnabled(boolean drawEnabled) {
+ if (mDrawEnabled != drawEnabled) {
+ mDrawEnabled = drawEnabled;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mCurrentRect != null && mDrawEnabled) {
+ canvas.drawRoundRect(mCurrentRect, mCornerRadius, mCornerRadius, mPaint);
+ if (mIcon != null && mIconAnimationProgress > 0f) {
+ canvas.save();
+ float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, 0f,
+ 1f).getInterpolation(mIconAnimationProgress);
+ canvas.translate(mCurrentRect.centerX() - mIcon.getBounds().width() / 2 * scale,
+ mCurrentRect.top - mIcon.getBounds().height() / 2 * scale);
+ canvas.scale(scale, scale);
+ mIcon.draw(canvas);
+ canvas.restore();
+ }
+ }
+ }
+
+ @Override
+ public void setAlpha(int i) { }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) { }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ private void setIconAnimationProgress(float progress) {
+ mIconAnimationProgress = progress;
+ invalidateSelf();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
similarity index 61%
rename from quickstep/src/com/android/quickstep/views/RecentsView.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index e18708b1938fbf7b84e1f2c3b7411547bcfa898f..a98df0fa1fd4e9bdddada73177cdf49ea68525a1 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -16,17 +16,32 @@
package com.android.quickstep.views;
+import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS;
+
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.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.Utilities.squaredTouchSlop;
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;
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.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
import android.animation.Animator;
import android.animation.AnimatorSet;
+import android.animation.LayoutTransition;
+import android.animation.LayoutTransition.TransitionListener;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
@@ -36,17 +51,17 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
+import android.graphics.Matrix;
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;
-import android.os.UserHandle;
-import android.support.annotation.Nullable;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
-import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.SparseBooleanArray;
@@ -55,39 +70,48 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewConfiguration;
import android.view.ViewDebug;
+import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ListView;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.SpringForce;
+
import com.android.launcher3.BaseActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherState;
import com.android.launcher3.PagedView;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.anim.SpringObjectAnimator;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.util.OverScroller;
import com.android.launcher3.util.PendingAnimation;
import com.android.launcher3.util.Themes;
-import com.android.quickstep.OverviewCallbacks;
-import com.android.quickstep.QuickScrubController;
+import com.android.launcher3.util.ViewPool;
+import com.android.quickstep.RecentsAnimationWrapper;
import com.android.quickstep.RecentsModel;
+import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener;
+import com.android.quickstep.TaskThumbnailCache;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.TaskViewDrawable;
-import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
-import com.android.systemui.shared.recents.model.RecentsTaskLoader;
import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.TaskStack;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.BackgroundExecutor;
+import com.android.systemui.shared.system.LauncherEventUtil;
import com.android.systemui.shared.system.PackageManagerWrapper;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
import com.android.systemui.shared.system.TaskStackChangeListener;
import java.util.ArrayList;
@@ -97,7 +121,9 @@ import java.util.function.Consumer;
* A list of recent tasks.
*/
@TargetApi(Build.VERSION_CODES.P)
-public abstract class RecentsView extends PagedView implements Insettable {
+public abstract class RecentsView extends PagedView implements Insettable,
+ TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+ InvariantDeviceProfile.OnIDPChangeListener, TaskThumbnailChangeListener {
private static final String TAG = RecentsView.class.getSimpleName();
@@ -114,39 +140,59 @@ public abstract class RecentsView extends PagedView impl
}
};
- private final Rect mTempRect = new Rect();
+ public static final FloatProperty FULLSCREEN_PROGRESS =
+ new FloatProperty("fullscreenProgress") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setFullscreenProgress(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mFullscreenProgress;
+ }
+ };
+
+ protected RecentsAnimationWrapper mRecentsAnimationWrapper;
+ protected ClipAnimationHelper mClipAnimationHelper;
+ protected SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier;
+ protected int mTaskWidth;
+ protected int mTaskHeight;
+ protected boolean mEnableDrawingLiveTile = false;
+ protected final Rect mTempRect = new Rect();
+ protected final RectF mTempRectF = new RectF();
private static final int DISMISS_TASK_DURATION = 300;
+ private static final int ADDITION_TASK_DURATION = 200;
// The threshold at which we update the SystemUI flags when animating from the task into the app
public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f;
- private static final float[] sTempFloatArray = new float[3];
-
protected final T mActivity;
- private final QuickScrubController mQuickScrubController;
private final float mFastFlingVelocity;
private final RecentsModel mModel;
private final int mTaskTopMargin;
private final ClearAllButton mClearAllButton;
private final Rect mClearAllButtonDeadZoneRect = new Rect();
private final Rect mTaskViewDeadZoneRect = new Rect();
+ protected final ClipAnimationHelper mTempClipAnimationHelper;
private final ScrollState mScrollState = new ScrollState();
// Keeps track of the previously known visible tasks for purposes of loading/unloading task data
private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
+ private final InvariantDeviceProfile mIdp;
+
+ private final ViewPool