Add system shortcuts when long pressing recent icon

We add a floating view for the menu that aligns with the task icon.

If available, the following shortcuts are present:
- Widgets
- App info
- Install (for instant apps)

It is designed to be straightforward to add to this list.

Bug: 70294936

Change-Id: I56c1098353d09fc564e0e92e59e4fcf692e486ba
diff --git a/quickstep/res/layout/task_menu.xml b/quickstep/res/layout/task_menu.xml
new file mode 100644
index 0000000..6e3fb4f
--- /dev/null
+++ b/quickstep/res/layout/task_menu.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.quickstep.TaskMenuView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/bg_popup_item_width"
+    android:layout_height="wrap_content"
+    android:visibility="invisible"
+    android:elevation="@dimen/deep_shortcuts_elevation"
+    android:orientation="vertical"
+    android:background="?attr/popupColorPrimary"
+    android:divider="@drawable/all_apps_divider"
+    android:showDividers="middle"
+    android:animateLayoutChanges="true">
+        <TextView
+            android:id="@+id/task_icon_and_name"
+            android:layout_width="match_parent"
+            android:layout_height="112dp"
+            android:textSize="14sp"
+            android:paddingTop="18dp"
+            android:drawablePadding="8dp"
+            android:gravity="center_horizontal"/>
+</com.android.quickstep.TaskMenuView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index a5716ea..587261d 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -18,6 +18,7 @@
 
     <dimen name="task_thumbnail_top_margin">24dp</dimen>
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
+    <dimen name="task_menu_background_radius">12dp</dimen>
 
     <dimen name="quickstep_fling_threshold_velocity">500dp</dimen>
     <dimen name="quickstep_fling_min_velocity">250dp</dimen>
diff --git a/quickstep/src/com/android/quickstep/TaskMenuView.java b/quickstep/src/com/android/quickstep/TaskMenuView.java
new file mode 100644
index 0000000..70542c2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskMenuView.java
@@ -0,0 +1,223 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * Contains options for a recent task when long-pressing its icon.
+ */
+public class TaskMenuView extends AbstractFloatingView {
+
+    private static final Rect sTempRect = new Rect();
+
+    /** 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.Widgets(),
+            new TaskSystemShortcut.AppInfo(),
+            new TaskSystemShortcut.Install()
+    };
+
+    private static final long OPEN_CLOSE_DURATION = 220;
+
+    private Launcher mLauncher;
+    private TextView mTaskIconAndName;
+    private AnimatorSet mOpenCloseAnimator;
+
+    public TaskMenuView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        mLauncher = Launcher.getLauncher(context);
+        setClipToOutline(true);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                float r = getResources().getDimensionPixelSize(R.dimen.task_menu_background_radius);
+                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), r);
+            }
+        });
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mTaskIconAndName = findViewById(R.id.task_icon_and_name);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            DragLayer dl = mLauncher.getDragLayer();
+            if (!dl.isEventOverView(this, ev)) {
+                // TODO: log this once we have a new container type for it?
+                close(true);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (animate) {
+            animateClose();
+        } else {
+            closeComplete();
+        }
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // TODO
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_TASK_MENU) != 0;
+    }
+
+    public static boolean showForTask(TaskView taskView) {
+        Launcher launcher = Launcher.getLauncher(taskView.getContext());
+        final TaskMenuView taskMenuView = (TaskMenuView) launcher.getLayoutInflater().inflate(
+                        R.layout.task_menu, launcher.getDragLayer(), false);
+        return taskMenuView.populateAndShowForTask(taskView);
+    }
+
+    private boolean populateAndShowForTask(TaskView taskView) {
+        if (isAttachedToWindow()) {
+            return false;
+        }
+        mLauncher.getDragLayer().addView(this);
+        addMenuOptions(taskView.getTask());
+        orientAroundTaskView(taskView);
+        post(this::animateOpen);
+        return true;
+    }
+
+    private void addMenuOptions(Task task) {
+        Drawable icon = task.icon.getConstantState().newDrawable();
+        int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size);
+        icon.setBounds(0, 0, iconSize, iconSize);
+        mTaskIconAndName.setCompoundDrawables(null, icon, null, null);
+        mTaskIconAndName.setText(TaskUtils.getTitle(mLauncher, task));
+
+        LayoutInflater inflater = mLauncher.getLayoutInflater();
+        for (TaskSystemShortcut menuOption : MENU_OPTIONS) {
+            OnClickListener onClickListener = menuOption.getOnClickListener(mLauncher, task);
+            if (onClickListener != null) {
+                DeepShortcutView menuOptionView = (DeepShortcutView) inflater.inflate(
+                        R.layout.system_shortcut, this, false);
+                menuOptionView.getIconView().setBackgroundResource(menuOption.iconResId);
+                menuOptionView.getBubbleText().setText(menuOption.labelResId);
+                menuOptionView.setOnClickListener(onClickListener);
+                addView(menuOptionView);
+            }
+        }
+    }
+
+    private void orientAroundTaskView(TaskView taskView) {
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        mLauncher.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
+        Rect insets = mLauncher.getDragLayer().getInsets();
+        setX(sTempRect.left + (sTempRect.width() - getMeasuredWidth()) / 2 - insets.left);
+        setY(sTempRect.top - mTaskIconAndName.getPaddingTop() - insets.top);
+    }
+
+    private void animateOpen() {
+        animateOpenOrClosed(false);
+        mIsOpen = true;
+    }
+
+    private void animateClose() {
+        animateOpenOrClosed(true);
+    }
+
+    private void animateOpenOrClosed(boolean closing) {
+        if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
+            return;
+        }
+        mOpenCloseAnimator = LauncherAnimUtils.createAnimatorSet();
+        mOpenCloseAnimator.play(createOpenCloseOutlineProvider()
+                .createRevealAnimator(this, closing));
+        mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                setVisibility(VISIBLE);
+            }
+
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                if (closing) {
+                    closeComplete();
+                }
+            }
+        });
+        mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
+        mOpenCloseAnimator.setDuration(OPEN_CLOSE_DURATION);
+        mOpenCloseAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
+        mOpenCloseAnimator.start();
+    }
+
+    private void closeComplete() {
+        mIsOpen = false;
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
+        int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size);
+        float fromRadius = iconSize / 2;
+        float toRadius = getResources().getDimensionPixelSize(
+                R.dimen.task_menu_background_radius);
+        Point iconCenter = new Point(getWidth() / 2, mTaskIconAndName.getPaddingTop() + iconSize / 2);
+        Rect fromRect = new Rect(iconCenter.x, iconCenter.y, iconCenter.x, iconCenter.y);
+        Rect toRect = new Rect(0, 0, getWidth(), getHeight());
+        return new RoundedRectRevealOutlineProvider(fromRadius, toRadius, fromRect, toRect) {
+            @Override
+            public boolean shouldRemoveElevationDuringAnimation() {
+                return true;
+            }
+        };
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
new file mode 100644
index 0000000..1ba7ce4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
@@ -0,0 +1,92 @@
+/*
+ * 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.content.ComponentName;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.view.View;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.util.InstantAppResolver;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * Represents a system shortcut that can be shown for a recent task.
+ */
+public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut {
+
+    protected T mSystemShortcut;
+
+    protected TaskSystemShortcut(T systemShortcut) {
+        super(systemShortcut.iconResId, systemShortcut.labelResId);
+        mSystemShortcut = systemShortcut;
+    }
+
+    @Override
+    public View.OnClickListener getOnClickListener(Launcher launcher, ItemInfo itemInfo) {
+        return null;
+    }
+
+    public View.OnClickListener getOnClickListener(final Launcher launcher, final Task task) {
+        ShortcutInfo dummyInfo = new ShortcutInfo();
+        dummyInfo.intent = new Intent();
+        ComponentName component = task.getTopComponent();
+        dummyInfo.intent.setComponent(component);
+        dummyInfo.user = UserHandle.getUserHandleForUid(task.key.userId);
+        dummyInfo.title = TaskUtils.getTitle(launcher, task);
+
+        return getOnClickListenerForTask(launcher, task, dummyInfo);
+    }
+
+    protected View.OnClickListener getOnClickListenerForTask(final Launcher launcher,
+            final Task task, final ItemInfo dummyInfo) {
+        return mSystemShortcut.getOnClickListener(launcher, dummyInfo);
+    }
+
+
+    public static class Widgets extends TaskSystemShortcut<SystemShortcut.Widgets> {
+        public Widgets() {
+            super(new SystemShortcut.Widgets());
+        }
+    }
+
+    public static class AppInfo extends TaskSystemShortcut<SystemShortcut.AppInfo> {
+        public AppInfo() {
+            super(new SystemShortcut.AppInfo());
+        }
+    }
+
+    public static class Install extends TaskSystemShortcut<SystemShortcut.Install> {
+        public Install() {
+            super(new SystemShortcut.Install());
+        }
+
+        @Override
+        protected View.OnClickListener getOnClickListenerForTask(Launcher launcher, Task task,
+                ItemInfo itemInfo) {
+            if (InstantAppResolver.newInstance(launcher).isInstantApp(launcher,
+                        task.getTopComponent().getPackageName())) {
+                return mSystemShortcut.createOnClickListener(launcher, itemInfo);
+            }
+            return null;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
new file mode 100644
index 0000000..a95e7c1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -0,0 +1,42 @@
+/*
+ * 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.content.pm.PackageManager;
+import android.util.Log;
+
+import com.android.launcher3.Launcher;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * Contains helpful methods for retrieving data from {@link Task}s.
+ * TODO: remove this once we switch to getting the icon and label from IconCache.
+ */
+public class TaskUtils {
+    private static final String TAG = "TaskUtils";
+
+    public static CharSequence getTitle(Launcher launcher, Task task) {
+        PackageManager pm = launcher.getPackageManager();
+        try {
+            return pm.getPackageInfo(task.getTopComponent().getPackageName(), 0)
+                    .applicationInfo.loadLabel(pm);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Failed to get title for task " + task, e);
+        }
+        return "";
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskView.java b/quickstep/src/com/android/quickstep/TaskView.java
index 6b37ada..94d85ee 100644
--- a/quickstep/src/com/android/quickstep/TaskView.java
+++ b/quickstep/src/com/android/quickstep/TaskView.java
@@ -208,12 +208,14 @@
     public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
         mSnapshotView.setThumbnail(thumbnailData);
         mIconView.setImageDrawable(task.icon);
+        mIconView.setOnLongClickListener(icon -> TaskMenuView.showForTask(this));
     }
 
     @Override
     public void onTaskDataUnloaded() {
         mSnapshotView.setThumbnail(null);
         mIconView.setImageDrawable(null);
+        mIconView.setOnLongClickListener(null);
     }
 
     @Override
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index da464c0..9a6be0b 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -43,7 +43,8 @@
             TYPE_WIDGET_RESIZE_FRAME,
             TYPE_WIDGETS_FULL_SHEET,
             TYPE_QUICKSTEP_PREVIEW,
-            TYPE_ON_BOARD_POPUP
+            TYPE_ON_BOARD_POPUP,
+            TYPE_TASK_MENU
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
@@ -54,10 +55,11 @@
     public static final int TYPE_WIDGETS_FULL_SHEET = 1 << 4;
     public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 5;
     public static final int TYPE_ON_BOARD_POPUP = 1 << 6;
+    public static final int TYPE_TASK_MENU = 1 << 7;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
-            | TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP;
+            | TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP | TYPE_TASK_MENU;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index c398aaa..42aa12b 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,5 +1,8 @@
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Action;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
+
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -19,9 +22,6 @@
 
 import java.util.List;
 
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-
 /**
  * Represents a system shortcut for a given app. The shortcut should have a static label and
  * icon, and an onClickListener that depends on the item that the shortcut services.
@@ -110,14 +110,15 @@
             if (!enabled) {
                 return null;
             }
-            return new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    Intent intent = PackageManagerHelper.getMarketIntent(itemInfo
-                            .getTargetComponent().getPackageName());
-                    launcher.startActivitySafely(view, intent, itemInfo);
-                    AbstractFloatingView.closeAllOpenViews(launcher);
-                }
+            return createOnClickListener(launcher, itemInfo);
+        }
+
+        public View.OnClickListener createOnClickListener(Launcher launcher, ItemInfo itemInfo) {
+            return view -> {
+                Intent intent = PackageManagerHelper.getMarketIntent(itemInfo
+                        .getTargetComponent().getPackageName());
+                launcher.startActivitySafely(view, intent, itemInfo);
+                AbstractFloatingView.closeAllOpenViews(launcher);
             };
         }
     }
diff --git a/src/com/android/launcher3/util/InstantAppResolver.java b/src/com/android/launcher3/util/InstantAppResolver.java
index 99ce7ca..601a5ab 100644
--- a/src/com/android/launcher3/util/InstantAppResolver.java
+++ b/src/com/android/launcher3/util/InstantAppResolver.java
@@ -18,8 +18,11 @@
 
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 
@@ -44,6 +47,17 @@
         return false;
     }
 
+    public boolean isInstantApp(Launcher launcher, String packageName) {
+        PackageManager packageManager = launcher.getPackageManager();
+        try {
+            return isInstantApp(packageManager.getPackageInfo(packageName, 0).applicationInfo);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e("InstantAppResolver", "Failed to determine whether package is instant app "
+                    + packageName, e);
+        }
+        return false;
+    }
+
     public List<ApplicationInfo> getInstantApps() {
         return Collections.emptyList();
     }