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