• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.taskbar;
17 
18 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
19 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
20 import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
21 import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
22 
23 import android.content.Intent;
24 import android.content.pm.LauncherApps;
25 import android.graphics.Point;
26 import android.util.Pair;
27 import android.util.SparseArray;
28 import android.view.MotionEvent;
29 import android.view.View;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 
34 import com.android.internal.logging.InstanceId;
35 import com.android.launcher3.AbstractFloatingView;
36 import com.android.launcher3.BubbleTextView;
37 import com.android.launcher3.Flags;
38 import com.android.launcher3.LauncherSettings;
39 import com.android.launcher3.R;
40 import com.android.launcher3.model.data.AppInfo;
41 import com.android.launcher3.model.data.ItemInfo;
42 import com.android.launcher3.model.data.WorkspaceItemInfo;
43 import com.android.launcher3.notification.NotificationListener;
44 import com.android.launcher3.popup.PopupContainerWithArrow;
45 import com.android.launcher3.popup.PopupDataProvider;
46 import com.android.launcher3.popup.SystemShortcut;
47 import com.android.launcher3.shortcuts.DeepShortcutView;
48 import com.android.launcher3.splitscreen.SplitShortcut;
49 import com.android.launcher3.util.ComponentKey;
50 import com.android.launcher3.util.ShortcutUtil;
51 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
52 import com.android.launcher3.views.ActivityContext;
53 import com.android.quickstep.SystemUiProxy;
54 import com.android.quickstep.util.LogUtils;
55 import com.android.quickstep.util.SingleTask;
56 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
57 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
58 
59 import java.io.PrintWriter;
60 import java.util.ArrayList;
61 import java.util.Arrays;
62 import java.util.HashMap;
63 import java.util.List;
64 import java.util.Objects;
65 import java.util.stream.Collectors;
66 import java.util.stream.Stream;
67 
68 /**
69  * Implements interfaces required to show and allow interacting with a PopupContainerWithArrow.
70  * Controls the long-press menu on Taskbar and AllApps icons.
71  */
72 public class TaskbarPopupController implements TaskbarControllers.LoggableTaskbarController {
73 
74     private static final SystemShortcut.Factory<BaseTaskbarContext>
75             APP_INFO = SystemShortcut.AppInfo::new;
76 
77     private static final SystemShortcut.Factory<BaseTaskbarContext>
78             BUBBLE = SystemShortcut.BubbleShortcut::new;
79 
80     private final TaskbarActivityContext mContext;
81     private final PopupDataProvider mPopupDataProvider;
82 
83     // Initialized in init.
84     private TaskbarControllers mControllers;
85     private boolean mAllowInitialSplitSelection;
86     private AppInfo[] mAppInfosList = AppInfo.EMPTY_ARRAY;
87     // Saves the ItemInfos in the hotseat without the predicted items.
88     private SparseArray<ItemInfo> mHotseatInfosList;
89     private ManageWindowsTaskbarShortcut<BaseTaskbarContext> mManageWindowsTaskbarShortcut;
90 
91 
TaskbarPopupController(TaskbarActivityContext context)92     public TaskbarPopupController(TaskbarActivityContext context) {
93         mContext = context;
94         mPopupDataProvider = new PopupDataProvider(mContext);
95     }
96 
init(TaskbarControllers controllers)97     public void init(TaskbarControllers controllers) {
98         mControllers = controllers;
99 
100         NotificationListener.addNotificationsChangedListener(mPopupDataProvider);
101     }
102 
onDestroy()103     public void onDestroy() {
104         NotificationListener.removeNotificationsChangedListener(mPopupDataProvider);
105     }
106 
107     @NonNull
getPopupDataProvider()108     public PopupDataProvider getPopupDataProvider() {
109         return mPopupDataProvider;
110     }
111 
setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy)112     public void setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
113         mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy);
114     }
115 
116     /** Closes the multi-instance menu if it is enabled and currently open. */
maybeCloseMultiInstanceMenu()117     public void maybeCloseMultiInstanceMenu() {
118         if (Flags.enableMultiInstanceMenuTaskbar() && mManageWindowsTaskbarShortcut != null) {
119             mManageWindowsTaskbarShortcut.closeMultiInstanceMenu();
120             cleanUpMultiInstanceMenuReference();
121         }
122     }
123 
124     /** Releases the reference to the Taskbar multi-instance menu */
cleanUpMultiInstanceMenuReference()125     public void cleanUpMultiInstanceMenuReference() {
126         mManageWindowsTaskbarShortcut = null;
127     }
128 
setAllowInitialSplitSelection(boolean allowInitialSplitSelection)129     public void setAllowInitialSplitSelection(boolean allowInitialSplitSelection) {
130         mAllowInitialSplitSelection = allowInitialSplitSelection;
131     }
132 
133     /**
134      * Shows the notifications and deep shortcuts associated with a Taskbar {@param icon}.
135      * @return the container if shown or null.
136      */
showForIcon(BubbleTextView icon)137     public PopupContainerWithArrow<BaseTaskbarContext> showForIcon(BubbleTextView icon) {
138         BaseTaskbarContext context = ActivityContext.lookupContext(icon.getContext());
139         if (PopupContainerWithArrow.getOpen(context) != null) {
140             // There is already an items container open, so don't open this one.
141             icon.clearFocus();
142             return null;
143         }
144 
145         ItemInfo itemInfo;
146         if (icon.getTag() instanceof ItemInfo item && ShortcutUtil.supportsShortcuts(item)) {
147             itemInfo = item;
148         } else if (icon.getTag() instanceof SingleTask task) {
149             itemInfo = SingleTask.Companion.createTaskItemInfo(task);
150         } else {
151             return null;
152         }
153 
154         PopupContainerWithArrow<BaseTaskbarContext> container;
155         int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(itemInfo);
156         // TODO(b/198438631): add support for INSTALL shortcut factory
157         List<SystemShortcut> systemShortcuts = getSystemShortcuts()
158                 .map(s -> s.getShortcut(context, itemInfo, icon))
159                 .filter(Objects::nonNull)
160                 .collect(Collectors.toList());
161 
162         // TODO(b/375648361): Revisit to see if this can be implemented within getSystemShortcuts().
163         if (Flags.enablePinningAppWithContextMenu()) {
164             SystemShortcut shortcut = createPinShortcut(context, itemInfo, icon);
165             if (shortcut != null) {
166                 systemShortcuts.add(0, shortcut);
167             }
168         }
169 
170         container = (PopupContainerWithArrow) context.getLayoutInflater().inflate(
171                     R.layout.popup_container, context.getDragLayer(), false);
172         container.populateAndShowRows(icon, itemInfo, deepShortcutCount, systemShortcuts);
173 
174         // TODO (b/198438631): configure for taskbar/context
175         container.setPopupItemDragHandler(new TaskbarPopupItemDragHandler());
176         mControllers.taskbarDragController.addDragListener(container);
177         container.requestFocus();
178 
179         // Make focusable to receive back events
180         context.onPopupVisibilityChanged(true);
181         container.addOnCloseCallback(() -> {
182             context.getDragLayer().post(() -> context.onPopupVisibilityChanged(false));
183         });
184 
185         return container;
186     }
187 
188     // Create a Stream of all applicable system shortcuts
getSystemShortcuts()189     private Stream<SystemShortcut.Factory> getSystemShortcuts() {
190         // append split options to APP_INFO shortcut if not in Desktop Windowing mode, the order
191         // here will reflect in the popup
192         ArrayList<SystemShortcut.Factory> shortcuts = new ArrayList<>();
193         shortcuts.add(APP_INFO);
194         if (!mControllers.taskbarDesktopModeController
195                 .isInDesktopModeAndNotInOverview(mContext.getDisplayId())) {
196             shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList());
197         }
198         if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
199             shortcuts.add(BUBBLE);
200         }
201 
202         if (Flags.enableMultiInstanceMenuTaskbar()
203                 && DesktopModeStatus.canEnterDesktopMode(mContext)
204                 && !mControllers.taskbarStashController.isInOverview()) {
205             maybeCloseMultiInstanceMenu();
206             shortcuts.addAll(getMultiInstanceMenuOptions().toList());
207         }
208         return shortcuts.stream();
209     }
210 
211     @Nullable
createPinShortcut(BaseTaskbarContext target, ItemInfo itemInfo, BubbleTextView originalView)212     private SystemShortcut createPinShortcut(BaseTaskbarContext target, ItemInfo itemInfo,
213             BubbleTextView originalView) {
214         // Predicted items use {@code HotseatPredictionController.PinPrediction} shortcut to pin.
215         if (itemInfo.isPredictedItem()) {
216             return null;
217         }
218         if (itemInfo.container == CONTAINER_HOTSEAT) {
219             return new PinToTaskbarShortcut<>(target, itemInfo, originalView, false,
220                     mHotseatInfosList);
221         }
222         if (mHotseatInfosList.size()
223                 < mContext.getTaskbarSpecsEvaluator().getNumShownHotseatIcons()) {
224             return new PinToTaskbarShortcut<>(target, itemInfo, originalView, true,
225                     mHotseatInfosList);
226         }
227 
228         return null;
229     }
230 
231     @Override
dumpLogs(String prefix, PrintWriter pw)232     public void dumpLogs(String prefix, PrintWriter pw) {
233         pw.println(prefix + "TaskbarPopupController:");
234 
235         mPopupDataProvider.dump(prefix + "\t", pw);
236     }
237 
238     private class TaskbarPopupItemDragHandler implements
239             PopupContainerWithArrow.PopupItemDragHandler {
240 
241         protected final Point mIconLastTouchPos = new Point();
242 
TaskbarPopupItemDragHandler()243         TaskbarPopupItemDragHandler() {}
244 
245         @Override
onTouch(View view, MotionEvent ev)246         public boolean onTouch(View view, MotionEvent ev) {
247             // Touched a shortcut, update where it was touched so we can drag from there on
248             // long click.
249             switch (ev.getAction()) {
250                 case MotionEvent.ACTION_DOWN:
251                 case MotionEvent.ACTION_MOVE:
252                     mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
253                     break;
254             }
255             return false;
256         }
257 
258         @Override
onLongClick(View v)259         public boolean onLongClick(View v) {
260             // Return early if not the correct view
261             if (!(v.getParent() instanceof DeepShortcutView)) return false;
262 
263             DeepShortcutView sv = (DeepShortcutView) v.getParent();
264             sv.setWillDrawIcon(false);
265 
266             // Move the icon to align with the center-top of the touch point
267             Point iconShift = new Point();
268             iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
269             iconShift.y = mIconLastTouchPos.y - mContext.getDeviceProfile().taskbarIconSize;
270 
271             ((TaskbarDragController) ActivityContext.lookupContext(
272                     v.getContext()).getDragController()).startDragOnLongClick(sv, iconShift);
273 
274             return false;
275         }
276     }
277 
278     /**
279      * Creates a factory function representing a single "split position" menu item ("Split left,"
280      * "Split right," or "Split top").
281      * @param position A SplitPositionOption representing whether we are splitting top, left, or
282      *                 right.
283      * @return A factory function to be used in populating the long-press menu.
284      */
createSplitShortcutFactory( SplitPositionOption position)285     SystemShortcut.Factory<BaseTaskbarContext> createSplitShortcutFactory(
286             SplitPositionOption position) {
287         return (context, itemInfo, originalView) -> new TaskbarSplitShortcut(context, itemInfo,
288                 originalView, position, mAllowInitialSplitSelection);
289     }
290 
291     /**
292      * Set the list of AppInfos to be able to pull from later
293      */
setApps(AppInfo[] apps)294     public void setApps(AppInfo[] apps) {
295         mAppInfosList = apps;
296     }
297 
298     /**
299      * Finds and returns an AppInfo object from a list, using its ComponentKey for identification.
300      * Based off of {@link com.android.launcher3.allapps.AllAppsStore#getApp(ComponentKey)}
301      * since we cannot access AllAppsStore from here.
302      */
getApp(ComponentKey key)303     public AppInfo getApp(ComponentKey key) {
304         if (key == null) {
305             return null;
306         }
307         AppInfo tempInfo = new AppInfo();
308         tempInfo.componentName = key.componentName;
309         tempInfo.user = key.user;
310         int index = Arrays.binarySearch(mAppInfosList, tempInfo, COMPONENT_KEY_COMPARATOR);
311         return index < 0 ? null : mAppInfosList[index];
312     }
313 
setHotseatInfosList(SparseArray<ItemInfo> info)314     public void setHotseatInfosList(SparseArray<ItemInfo> info) {
315         mHotseatInfosList = info;
316     }
317 
318     /**
319      * Returns a stream of Multi Instance menu options if an app supports it.
320      */
getMultiInstanceMenuOptions()321     Stream<SystemShortcut.Factory<BaseTaskbarContext>> getMultiInstanceMenuOptions() {
322         SystemShortcut.Factory<BaseTaskbarContext> f1 = createNewWindowShortcutFactory();
323         SystemShortcut.Factory<BaseTaskbarContext> f2 = createManageWindowsShortcutFactory();
324         return f1 != null ? Stream.of(f1, f2) : Stream.empty();
325     }
326 
327     /**
328      * Creates a factory function representing a "New Window" menu item only if the calling app
329      * supports multi-instance.
330      * @return A factory function to be used in populating the long-press menu.
331      */
createNewWindowShortcutFactory()332     SystemShortcut.Factory<BaseTaskbarContext> createNewWindowShortcutFactory() {
333         return (context, itemInfo, originalView) -> {
334             if (shouldShowMultiInstanceOptions(itemInfo)) {
335                 return new NewWindowTaskbarShortcut<>(context, itemInfo, originalView);
336             }
337             return null;
338         };
339     }
340 
341     /**
342      * Creates a factory function representing a "Manage Windows" menu item only if the calling app
343      * supports multi-instance. This menu item shows the open instances of the calling app.
344      * @return A factory function to be used in populating the long-press menu.
345      */
346     public SystemShortcut.Factory<BaseTaskbarContext> createManageWindowsShortcutFactory() {
347         return (context, itemInfo, originalView) -> {
348             if (shouldShowMultiInstanceOptions(itemInfo)) {
349                 mManageWindowsTaskbarShortcut = new ManageWindowsTaskbarShortcut<>(
350                         context, itemInfo, originalView, mControllers);
351                 return mManageWindowsTaskbarShortcut;
352             }
353             return null;
354         };
355     }
356 
357     /**
358      * Determines whether to show multi-instance options for a given item.
359      */
360     private boolean shouldShowMultiInstanceOptions(ItemInfo itemInfo) {
361         ComponentKey key = itemInfo.getComponentKey();
362         AppInfo app = getApp(key);
363         return app != null && app.supportsMultiInstance()
364                 && itemInfo.container != CONTAINER_ALL_APPS;
365     }
366 
367     /**
368      * A single menu item ("Split left," "Split right," or "Split top") that executes a split
369      * from the taskbar, as if the user performed a drag and drop split.
370      * Includes an onClick method that initiates the actual split.
371      */
372     private static class TaskbarSplitShortcut extends
373              SplitShortcut<BaseTaskbarContext> {
374          /**
375           * If {@code true}, clicking this shortcut will not attempt to start a split app directly,
376           * but be the first app in split selection mode
377           */
378          private final boolean mAllowInitialSplitSelection;
379 
380          TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView,
381                 SplitPositionOption position, boolean allowInitialSplitSelection) {
382              super(position.iconResId, position.textResId, context, itemInfo, originalView,
383                      position);
384              mAllowInitialSplitSelection = allowInitialSplitSelection;
385          }
386 
387         @Override
388         public void onClick(View view) {
389             // Add callbacks depending on what type of Taskbar context we're in (Taskbar or AllApps)
390             mTarget.onSplitScreenMenuButtonClicked();
391             AbstractFloatingView.closeAllOpenViews(mTarget);
392 
393             // Depending on what app state we're in, we either want to initiate the split screen
394             // staging process or immediately launch a split with an existing app.
395             // - Initiate the split screen staging process
396              if (mAllowInitialSplitSelection) {
397                  super.onClick(view);
398                  return;
399              }
400 
401             // - Immediately launch split with the running app
402             Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
403                     LogUtils.getShellShareableInstanceId();
404             mTarget.getStatsLogManager().logger()
405                     .withItemInfo(mItemInfo)
406                     .withInstanceId(instanceIds.second)
407                     .log(getLogEventForPosition(getPosition().stagePosition));
408 
409             if (mItemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
410                 WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo;
411                 SystemUiProxy.INSTANCE.get(mTarget).startShortcut(
412                         workspaceItemInfo.getIntent().getPackage(),
413                         workspaceItemInfo.getDeepShortcutId(),
414                         getPosition().stagePosition,
415                         null,
416                         workspaceItemInfo.user,
417                         instanceIds.first);
418             } else {
419                 SystemUiProxy.INSTANCE.get(mTarget).startIntent(
420                         mTarget.getSystemService(LauncherApps.class).getMainActivityLaunchIntent(
421                                 mItemInfo.getIntent().getComponent(),
422                                 null,
423                                 mItemInfo.user),
424                         mItemInfo.user.getIdentifier(),
425                         new Intent(),
426                         getPosition().stagePosition,
427                         null,
428                         instanceIds.first);
429             }
430         }
431     }
432 }
433 
434