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.util.SplitConfigurationOptions.getLogEventForPosition;
19 
20 import android.content.Intent;
21 import android.content.pm.LauncherApps;
22 import android.graphics.Point;
23 import android.util.Pair;
24 import android.view.MotionEvent;
25 import android.view.View;
26 
27 import androidx.annotation.NonNull;
28 
29 import com.android.internal.logging.InstanceId;
30 import com.android.launcher3.AbstractFloatingView;
31 import com.android.launcher3.BubbleTextView;
32 import com.android.launcher3.LauncherSettings;
33 import com.android.launcher3.R;
34 import com.android.launcher3.dot.FolderDotInfo;
35 import com.android.launcher3.folder.Folder;
36 import com.android.launcher3.folder.FolderIcon;
37 import com.android.launcher3.model.data.FolderInfo;
38 import com.android.launcher3.model.data.ItemInfo;
39 import com.android.launcher3.model.data.WorkspaceItemInfo;
40 import com.android.launcher3.notification.NotificationListener;
41 import com.android.launcher3.popup.PopupContainerWithArrow;
42 import com.android.launcher3.popup.PopupDataProvider;
43 import com.android.launcher3.popup.SystemShortcut;
44 import com.android.launcher3.shortcuts.DeepShortcutView;
45 import com.android.launcher3.splitscreen.SplitShortcut;
46 import com.android.launcher3.util.ComponentKey;
47 import com.android.launcher3.util.LauncherBindableItemsContainer;
48 import com.android.launcher3.util.PackageUserKey;
49 import com.android.launcher3.util.ShortcutUtil;
50 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
51 import com.android.launcher3.views.ActivityContext;
52 import com.android.quickstep.SystemUiProxy;
53 import com.android.quickstep.util.LogUtils;
54 
55 import java.io.PrintWriter;
56 import java.util.HashMap;
57 import java.util.List;
58 import java.util.Objects;
59 import java.util.function.Predicate;
60 import java.util.stream.Collectors;
61 import java.util.stream.Stream;
62 
63 /**
64  * Implements interfaces required to show and allow interacting with a PopupContainerWithArrow.
65  * Controls the long-press menu on Taskbar and AllApps icons.
66  */
67 public class TaskbarPopupController implements TaskbarControllers.LoggableTaskbarController {
68 
69     private static final SystemShortcut.Factory<BaseTaskbarContext>
70             APP_INFO = SystemShortcut.AppInfo::new;
71 
72     private final TaskbarActivityContext mContext;
73     private final PopupDataProvider mPopupDataProvider;
74 
75     // Initialized in init.
76     private TaskbarControllers mControllers;
77     private boolean mAllowInitialSplitSelection;
78 
TaskbarPopupController(TaskbarActivityContext context)79     public TaskbarPopupController(TaskbarActivityContext context) {
80         mContext = context;
81         mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
82     }
83 
init(TaskbarControllers controllers)84     public void init(TaskbarControllers controllers) {
85         mControllers = controllers;
86 
87         NotificationListener.addNotificationsChangedListener(mPopupDataProvider);
88     }
89 
onDestroy()90     public void onDestroy() {
91         NotificationListener.removeNotificationsChangedListener(mPopupDataProvider);
92     }
93 
94     @NonNull
getPopupDataProvider()95     public PopupDataProvider getPopupDataProvider() {
96         return mPopupDataProvider;
97     }
98 
setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy)99     public void setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
100         mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy);
101     }
102 
setAllowInitialSplitSelection(boolean allowInitialSplitSelection)103     public void setAllowInitialSplitSelection(boolean allowInitialSplitSelection) {
104         mAllowInitialSplitSelection = allowInitialSplitSelection;
105     }
106 
updateNotificationDots(Predicate<PackageUserKey> updatedDots)107     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
108         final PackageUserKey packageUserKey = new PackageUserKey(null, null);
109         Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
110                 || updatedDots.test(packageUserKey);
111 
112         LauncherBindableItemsContainer.ItemOperator op = (info, v) -> {
113             if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
114                 if (matcher.test(info)) {
115                     ((BubbleTextView) v).applyDotState(info, true /* animate */);
116                 }
117             } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
118                 FolderInfo fi = (FolderInfo) info;
119                 if (fi.anyMatch(matcher)) {
120                     FolderDotInfo folderDotInfo = new FolderDotInfo();
121                     for (ItemInfo si : fi.getContents()) {
122                         folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si));
123                     }
124                     ((FolderIcon) v).setDotInfo(folderDotInfo);
125                 }
126             }
127 
128             // process all the shortcuts
129             return false;
130         };
131 
132         mControllers.taskbarViewController.mapOverItems(op);
133         Folder folder = Folder.getOpen(mContext);
134         if (folder != null) {
135             folder.iterateOverItems(op);
136         }
137         mControllers.taskbarAllAppsController.updateNotificationDots(updatedDots);
138     }
139 
140     /**
141      * Shows the notifications and deep shortcuts associated with a Taskbar {@param icon}.
142      * @return the container if shown or null.
143      */
showForIcon(BubbleTextView icon)144     public PopupContainerWithArrow<BaseTaskbarContext> showForIcon(BubbleTextView icon) {
145         BaseTaskbarContext context = ActivityContext.lookupContext(icon.getContext());
146         if (PopupContainerWithArrow.getOpen(context) != null) {
147             // There is already an items container open, so don't open this one.
148             icon.clearFocus();
149             return null;
150         }
151         ItemInfo item = (ItemInfo) icon.getTag();
152         if (!ShortcutUtil.supportsShortcuts(item)) {
153             return null;
154         }
155 
156         PopupContainerWithArrow<BaseTaskbarContext> container;
157         int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(item);
158         // TODO(b/198438631): add support for INSTALL shortcut factory
159         List<SystemShortcut> systemShortcuts = getSystemShortcuts()
160                 .map(s -> s.getShortcut(context, item, icon))
161                 .filter(Objects::nonNull)
162                 .collect(Collectors.toList());
163 
164         container = (PopupContainerWithArrow) context.getLayoutInflater().inflate(
165                     R.layout.popup_container, context.getDragLayer(), false);
166         container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts);
167 
168         // TODO (b/198438631): configure for taskbar/context
169         container.setPopupItemDragHandler(new TaskbarPopupItemDragHandler());
170         mControllers.taskbarDragController.addDragListener(container);
171         container.requestFocus();
172 
173         // Make focusable to receive back events
174         context.onPopupVisibilityChanged(true);
175         container.addOnCloseCallback(() -> {
176             context.getDragLayer().post(() -> context.onPopupVisibilityChanged(false));
177         });
178 
179         return container;
180     }
181 
182     // Create a Stream of all applicable system shortcuts
getSystemShortcuts()183     private Stream<SystemShortcut.Factory> getSystemShortcuts() {
184         // append split options to APP_INFO shortcut, the order here will reflect in the popup
185         return Stream.concat(
186                 Stream.of(APP_INFO),
187                 mControllers.uiController.getSplitMenuOptions()
188         );
189     }
190 
191     @Override
dumpLogs(String prefix, PrintWriter pw)192     public void dumpLogs(String prefix, PrintWriter pw) {
193         pw.println(prefix + "TaskbarPopupController:");
194 
195         mPopupDataProvider.dump(prefix + "\t", pw);
196     }
197 
198     private class TaskbarPopupItemDragHandler implements
199             PopupContainerWithArrow.PopupItemDragHandler {
200 
201         protected final Point mIconLastTouchPos = new Point();
202 
TaskbarPopupItemDragHandler()203         TaskbarPopupItemDragHandler() {}
204 
205         @Override
onTouch(View view, MotionEvent ev)206         public boolean onTouch(View view, MotionEvent ev) {
207             // Touched a shortcut, update where it was touched so we can drag from there on
208             // long click.
209             switch (ev.getAction()) {
210                 case MotionEvent.ACTION_DOWN:
211                 case MotionEvent.ACTION_MOVE:
212                     mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
213                     break;
214             }
215             return false;
216         }
217 
218         @Override
onLongClick(View v)219         public boolean onLongClick(View v) {
220             // Return early if not the correct view
221             if (!(v.getParent() instanceof DeepShortcutView)) return false;
222 
223             DeepShortcutView sv = (DeepShortcutView) v.getParent();
224             sv.setWillDrawIcon(false);
225 
226             // Move the icon to align with the center-top of the touch point
227             Point iconShift = new Point();
228             iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
229             iconShift.y = mIconLastTouchPos.y - mContext.getDeviceProfile().taskbarIconSize;
230 
231             ((TaskbarDragController) ActivityContext.lookupContext(
232                     v.getContext()).getDragController()).startDragOnLongClick(sv, iconShift);
233 
234             return false;
235         }
236     }
237 
238     /**
239      * Creates a factory function representing a single "split position" menu item ("Split left,"
240      * "Split right," or "Split top").
241      * @param position A SplitPositionOption representing whether we are splitting top, left, or
242      *                 right.
243      * @return A factory function to be used in populating the long-press menu.
244      */
createSplitShortcutFactory( SplitPositionOption position)245     SystemShortcut.Factory<BaseTaskbarContext> createSplitShortcutFactory(
246             SplitPositionOption position) {
247         return (context, itemInfo, originalView) -> new TaskbarSplitShortcut(context, itemInfo,
248                 originalView, position, mAllowInitialSplitSelection);
249     }
250 
251      /**
252      * A single menu item ("Split left," "Split right," or "Split top") that executes a split
253      * from the taskbar, as if the user performed a drag and drop split.
254      * Includes an onClick method that initiates the actual split.
255      */
256     private static class TaskbarSplitShortcut extends
257              SplitShortcut<BaseTaskbarContext> {
258          /**
259           * If {@code true}, clicking this shortcut will not attempt to start a split app directly,
260           * but be the first app in split selection mode
261           */
262          private final boolean mAllowInitialSplitSelection;
263 
TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView, SplitPositionOption position, boolean allowInitialSplitSelection)264          TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView,
265                 SplitPositionOption position, boolean allowInitialSplitSelection) {
266              super(position.iconResId, position.textResId, context, itemInfo, originalView,
267                      position);
268              mAllowInitialSplitSelection = allowInitialSplitSelection;
269          }
270 
271         @Override
onClick(View view)272         public void onClick(View view) {
273             // Add callbacks depending on what type of Taskbar context we're in (Taskbar or AllApps)
274             mTarget.onSplitScreenMenuButtonClicked();
275             AbstractFloatingView.closeAllOpenViews(mTarget);
276 
277             // Depending on what app state we're in, we either want to initiate the split screen
278             // staging process or immediately launch a split with an existing app.
279             // - Initiate the split screen staging process
280              if (mAllowInitialSplitSelection) {
281                  super.onClick(view);
282                  return;
283              }
284 
285             // - Immediately launch split with the running app
286             Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
287                     LogUtils.getShellShareableInstanceId();
288             mTarget.getStatsLogManager().logger()
289                     .withItemInfo(mItemInfo)
290                     .withInstanceId(instanceIds.second)
291                     .log(getLogEventForPosition(getPosition().stagePosition));
292 
293             if (mItemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
294                 WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo;
295                 SystemUiProxy.INSTANCE.get(mTarget).startShortcut(
296                         workspaceItemInfo.getIntent().getPackage(),
297                         workspaceItemInfo.getDeepShortcutId(),
298                         getPosition().stagePosition,
299                         null,
300                         workspaceItemInfo.user,
301                         instanceIds.first);
302             } else {
303                 SystemUiProxy.INSTANCE.get(mTarget).startIntent(
304                         mTarget.getSystemService(LauncherApps.class).getMainActivityLaunchIntent(
305                                 mItemInfo.getIntent().getComponent(),
306                                 null,
307                                 mItemInfo.user),
308                         mItemInfo.user.getIdentifier(),
309                         new Intent(),
310                         getPosition().stagePosition,
311                         null,
312                         instanceIds.first);
313             }
314         }
315     }
316 }
317 
318