1 /*
2  * Copyright (C) 2018 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 
17 package com.android.quickstep;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
21 import static android.view.Surface.ROTATION_0;
22 
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
24 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
25 import static com.android.window.flags.Flags.enableDesktopWindowingMode;
26 
27 import android.app.ActivityOptions;
28 import android.graphics.Bitmap;
29 import android.graphics.Color;
30 import android.graphics.Rect;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.os.RemoteException;
34 import android.provider.Settings;
35 import android.util.Log;
36 import android.view.View;
37 import android.view.WindowInsets;
38 import android.view.WindowManagerGlobal;
39 import android.window.SplashScreen;
40 
41 import androidx.annotation.Nullable;
42 
43 import com.android.launcher3.DeviceProfile;
44 import com.android.launcher3.Flags;
45 import com.android.launcher3.R;
46 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
47 import com.android.launcher3.model.WellbeingModel;
48 import com.android.launcher3.popup.SystemShortcut;
49 import com.android.launcher3.popup.SystemShortcut.AppInfo;
50 import com.android.launcher3.util.InstantAppResolver;
51 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
52 import com.android.launcher3.views.ActivityContext;
53 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
54 import com.android.quickstep.util.RecentsOrientedState;
55 import com.android.quickstep.views.GroupedTaskView;
56 import com.android.quickstep.views.RecentsView;
57 import com.android.quickstep.views.RecentsViewContainer;
58 import com.android.quickstep.views.TaskThumbnailViewDeprecated;
59 import com.android.quickstep.views.TaskView;
60 import com.android.quickstep.views.TaskView.TaskContainer;
61 import com.android.systemui.shared.recents.model.Task;
62 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
63 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
64 import com.android.systemui.shared.recents.view.RecentsTransition;
65 import com.android.systemui.shared.system.ActivityManagerWrapper;
66 
67 import java.util.Collections;
68 import java.util.List;
69 import java.util.function.Function;
70 import java.util.stream.Collectors;
71 
72 /**
73  * Represents a system shortcut that can be shown for a recent task. Appears as a single entry in
74  * the dropdown menu that shows up when you tap an app icon in Overview.
75  */
76 public interface TaskShortcutFactory {
77     @Nullable
getShortcuts(RecentsViewContainer container, TaskContainer taskContainer)78     default List<SystemShortcut> getShortcuts(RecentsViewContainer container,
79             TaskContainer taskContainer) {
80         return null;
81     }
82 
83     /**
84      * Returns {@code true} if it should be shown for grouped task; {@code false} otherwise.
85      */
showForGroupedTask()86     default boolean showForGroupedTask() {
87         return false;
88     }
89 
90     /**
91      * Returns {@code true} if it should be shown for desktop task; {@code false} otherwise.
92      */
showForDesktopTask()93     default boolean showForDesktopTask() {
94         return false;
95     }
96 
97     /** @return a singleton list if the provided shortcut is non-null, null otherwise */
98     @Nullable
createSingletonShortcutList(@ullable SystemShortcut shortcut)99     default List<SystemShortcut> createSingletonShortcutList(@Nullable SystemShortcut shortcut) {
100         if (shortcut != null) {
101             return Collections.singletonList(shortcut);
102         }
103         return null;
104     }
105 
106     TaskShortcutFactory APP_INFO = new TaskShortcutFactory() {
107         @Override
108         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
109                 TaskContainer taskContainer) {
110             TaskView taskView = taskContainer.getTaskView();
111             int actionId = taskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT
112                     ? R.id.action_app_info_bottom_right
113                     : R.id.action_app_info_top_left;
114 
115             AppInfo.SplitAccessibilityInfo accessibilityInfo =
116                     new AppInfo.SplitAccessibilityInfo(taskView.containsMultipleTasks(),
117                             TaskUtils.getTitle(taskView.getContext(), taskContainer.getTask()),
118                             actionId
119                     );
120             return Collections.singletonList(new AppInfo(container, taskContainer.getItemInfo(),
121                     taskView, accessibilityInfo));
122         }
123 
124         @Override
125         public boolean showForGroupedTask() {
126             return true;
127         }
128     };
129 
130     class SplitSelectSystemShortcut extends SystemShortcut {
131         private final TaskView mTaskView;
132         private final SplitPositionOption mSplitPositionOption;
133 
SplitSelectSystemShortcut(RecentsViewContainer container, TaskView taskView, SplitPositionOption option)134         public SplitSelectSystemShortcut(RecentsViewContainer container, TaskView taskView,
135                 SplitPositionOption option) {
136             super(option.iconResId, option.textResId, container, taskView.getFirstItemInfo(),
137                     taskView);
138             mTaskView = taskView;
139             mSplitPositionOption = option;
140         }
141 
142         @Override
onClick(View view)143         public void onClick(View view) {
144             mTaskView.initiateSplitSelect(mSplitPositionOption);
145         }
146     }
147 
148     /**
149      * A menu item, "Save app pair", that allows the user to preserve the current app combination as
150      * one persistent icon on the Home screen, allowing for quick split screen launching.
151      */
152     class SaveAppPairSystemShortcut extends SystemShortcut<RecentsViewContainer> {
153         private final GroupedTaskView mTaskView;
154 
155 
SaveAppPairSystemShortcut(RecentsViewContainer container, GroupedTaskView taskView, int iconResId)156         public SaveAppPairSystemShortcut(RecentsViewContainer container, GroupedTaskView taskView,
157             int iconResId) {
158             super(iconResId, R.string.save_app_pair, container, taskView.getFirstItemInfo(),
159                     taskView);
160             mTaskView = taskView;
161         }
162 
163         @Override
onClick(View view)164         public void onClick(View view) {
165             dismissTaskMenuView();
166             ((RecentsView) mTarget.getOverviewPanel())
167                     .getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
168         }
169     }
170 
171     class FreeformSystemShortcut extends SystemShortcut<RecentsViewContainer> {
172         private static final String TAG = "FreeformSystemShortcut";
173 
174         private Handler mHandler;
175 
176         private final RecentsView mRecentsView;
177         private final TaskThumbnailViewDeprecated mThumbnailView;
178         private final TaskView mTaskView;
179         private final LauncherEvent mLauncherEvent;
180 
FreeformSystemShortcut(int iconRes, int textRes, RecentsViewContainer container, TaskContainer taskContainer, LauncherEvent launcherEvent)181         public FreeformSystemShortcut(int iconRes, int textRes, RecentsViewContainer container,
182                 TaskContainer taskContainer, LauncherEvent launcherEvent) {
183             super(iconRes, textRes, container, taskContainer.getItemInfo(),
184                     taskContainer.getTaskView());
185             mLauncherEvent = launcherEvent;
186             mHandler = new Handler(Looper.getMainLooper());
187             mTaskView = taskContainer.getTaskView();
188             mRecentsView = container.getOverviewPanel();
189             mThumbnailView = taskContainer.getThumbnailViewDeprecated();
190         }
191 
192         @Override
onClick(View view)193         public void onClick(View view) {
194             dismissTaskMenuView();
195             RecentsView rv = mTarget.getOverviewPanel();
196             rv.switchToScreenshot(() -> {
197                 rv.finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> {
198                     mTarget.returnToHomescreen();
199                     rv.getHandler().post(this::startActivity);
200                 });
201             });
202         }
203 
startActivity()204         private void startActivity() {
205             final Task.TaskKey taskKey = mTaskView.getFirstTask().key;
206             final int taskId = taskKey.id;
207             final ActivityOptions options = makeLaunchOptions(mTarget);
208             if (options != null) {
209                 options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
210             }
211             if (options != null
212                     && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
213                     options)) {
214                 final Runnable animStartedListener = () -> {
215                     // Hide the task view and wait for the window to be resized
216                     // TODO: Consider animating in launcher and do an in-place start activity
217                     //       afterwards
218                     mRecentsView.setIgnoreResetTask(taskId);
219                     mTaskView.setAlpha(0f);
220                 };
221 
222                 final int[] position = new int[2];
223                 mThumbnailView.getLocationOnScreen(position);
224                 final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
225                 final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
226                 final Rect taskBounds = new Rect(position[0], position[1],
227                         position[0] + width, position[1] + height);
228 
229                 // Take the thumbnail of the task without a scrim and apply it back after
230                 float alpha = mThumbnailView.getDimAlpha();
231                 mThumbnailView.setDimAlpha(0);
232                 Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
233                         taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
234                         Color.BLACK);
235                 mThumbnailView.setDimAlpha(alpha);
236 
237                 AppTransitionAnimationSpecsFuture future =
238                         new AppTransitionAnimationSpecsFuture(mHandler) {
239                             @Override
240                             public List<AppTransitionAnimationSpecCompat> composeSpecs() {
241                                 return Collections.singletonList(
242                                         new AppTransitionAnimationSpecCompat(
243                                                 taskId, thumbnail, taskBounds));
244                             }
245                         };
246                 overridePendingAppTransitionMultiThumbFuture(
247                         future, animStartedListener, mHandler, true /* scaleUp */,
248                         taskKey.displayId);
249                 mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getFirstItemInfo())
250                         .log(mLauncherEvent);
251             }
252         }
253 
254         /**
255          * Overrides a pending app transition.
256          */
overridePendingAppTransitionMultiThumbFuture( AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback, Handler animStartedCallbackHandler, boolean scaleUp, int displayId)257         private void overridePendingAppTransitionMultiThumbFuture(
258                 AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback,
259                 Handler animStartedCallbackHandler, boolean scaleUp, int displayId) {
260             try {
261                 WindowManagerGlobal.getWindowManagerService()
262                         .overridePendingAppTransitionMultiThumbFuture(
263                                 animationSpecFuture.getFuture(),
264                                 RecentsTransition.wrapStartedListener(animStartedCallbackHandler,
265                                         animStartedCallback), scaleUp, displayId);
266             } catch (RemoteException e) {
267                 Log.w(TAG, "Failed to override pending app transition (multi-thumbnail future): ",
268                         e);
269             }
270         }
271 
makeLaunchOptions(RecentsViewContainer container)272         private ActivityOptions makeLaunchOptions(RecentsViewContainer container) {
273             ActivityOptions activityOptions = ActivityOptions.makeBasic();
274             activityOptions.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
275             // Arbitrary bounds only because freeform is in dev mode right now
276             final View decorView = container.getWindow().getDecorView();
277             final WindowInsets insets = decorView.getRootWindowInsets();
278             final Rect r = new Rect(0, 0, decorView.getWidth() / 2, decorView.getHeight() / 2);
279             r.offsetTo(insets.getSystemWindowInsetLeft() + 50,
280                     insets.getSystemWindowInsetTop() + 50);
281             activityOptions.setLaunchBounds(r);
282             return activityOptions;
283         }
284     }
285 
286     /**
287      * Does NOT add split options in the following scenarios:
288      * * 1. Taskbar is not present AND aren't at least 2 tasks in overview to show split options for
289      * * 2. Split isn't supported by the task itself (non resizable activity)
290      * * 3. We aren't currently in multi-window
291      * * 4. The taskView to show split options for is the focused task AND we haven't started
292      * * scrolling in overview (if we haven't scrolled, there's a split overview action button so
293      * * we don't need this menu option)
294      */
295     TaskShortcutFactory SPLIT_SELECT = new TaskShortcutFactory() {
296         @Override
297         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
298                 TaskContainer taskContainer) {
299             DeviceProfile deviceProfile = container.getDeviceProfile();
300             final Task task = taskContainer.getTask();
301             final int intentFlags = task.key.baseIntent.getFlags();
302             final TaskView taskView = taskContainer.getTaskView();
303             final RecentsView recentsView = taskView.getRecentsView();
304             final RecentsPagedOrientationHandler orientationHandler =
305                     recentsView.getPagedOrientationHandler();
306 
307             boolean notEnoughTasksToSplit =
308                     !deviceProfile.isTaskbarPresent && recentsView.getTaskViewCount() < 2;
309             boolean isTaskSplitNotSupported = !task.isDockable ||
310                     (intentFlags & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
311             boolean hideForExistingMultiWindow = container.getDeviceProfile().isMultiWindowMode;
312             boolean isFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
313             boolean isTaskInExpectedScrollPosition =
314                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
315 
316             if (notEnoughTasksToSplit || isTaskSplitNotSupported || hideForExistingMultiWindow
317                     || (isFocusedTask && isTaskInExpectedScrollPosition)) {
318                 return null;
319             }
320 
321             return orientationHandler.getSplitPositionOptions(deviceProfile)
322                     .stream()
323                     .map((Function<SplitPositionOption, SystemShortcut>) option ->
324                             new SplitSelectSystemShortcut(container, taskView, option))
325                     .collect(Collectors.toList());
326         }
327     };
328 
329     TaskShortcutFactory SAVE_APP_PAIR = new TaskShortcutFactory() {
330         @Nullable
331         @Override
332         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
333                 TaskContainer taskContainer) {
334             DeviceProfile deviceProfile = container.getDeviceProfile();
335             final TaskView taskView = taskContainer.getTaskView();
336             final RecentsView recentsView = taskView.getRecentsView();
337             boolean isLargeTileFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
338             boolean isInExpectedScrollPosition =
339                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
340             boolean shouldShowActionsButtonInstead =
341                     isLargeTileFocusedTask && isInExpectedScrollPosition;
342 
343             // No "save app pair" menu item if:
344             // - we are in 3p launcher
345             // - the Overview Actions Button should be visible
346             // - the task view is not a valid save-able split pair
347             if (!recentsView.supportsAppPairs()
348                     || shouldShowActionsButtonInstead
349                     || !recentsView.getSplitSelectController().getAppPairsController()
350                             .canSaveAppPair(taskView)) {
351                 return null;
352             }
353 
354             int iconResId = deviceProfile.isLeftRightSplit
355                     ? R.drawable.ic_save_app_pair_left_right
356                     : R.drawable.ic_save_app_pair_up_down;
357 
358             return Collections.singletonList(
359                     new SaveAppPairSystemShortcut(container,
360                             (GroupedTaskView) taskView, iconResId));
361         }
362 
363         @Override
364         public boolean showForGroupedTask() {
365             return true;
366         }
367     };
368 
369     TaskShortcutFactory FREE_FORM = new TaskShortcutFactory() {
370         @Override
371         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
372                 TaskContainer taskContainer) {
373             final Task task = taskContainer.getTask();
374             if (!task.isDockable) {
375                 return null;
376             }
377             if (!isAvailable(container)) {
378                 return null;
379             }
380 
381             return Collections.singletonList(new FreeformSystemShortcut(
382                     R.drawable.ic_caption_desktop_button_foreground,
383                     R.string.recent_task_option_freeform, container, taskContainer,
384                     LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP));
385         }
386 
387         private boolean isAvailable(RecentsViewContainer container) {
388             return Settings.Global.getInt(
389                     container.asContext().getContentResolver(),
390                     Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0
391                     && !enableDesktopWindowingMode();
392         }
393     };
394 
395     TaskShortcutFactory PIN = new TaskShortcutFactory() {
396         @Override
397         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
398                 TaskContainer taskContainer) {
399             if (!SystemUiProxy.INSTANCE.get(container.asContext()).isActive()) {
400                 return null;
401             }
402             if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
403                 return null;
404             }
405             if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
406                 // We shouldn't be able to pin while an app is locked.
407                 return null;
408             }
409             return Collections.singletonList(new PinSystemShortcut(container, taskContainer));
410         }
411     };
412 
413     class PinSystemShortcut extends SystemShortcut<RecentsViewContainer> {
414 
415         private static final String TAG = "PinSystemShortcut";
416 
417         private final TaskView mTaskView;
418 
PinSystemShortcut(RecentsViewContainer target, TaskContainer taskContainer)419         public PinSystemShortcut(RecentsViewContainer target,
420                 TaskContainer taskContainer) {
421             super(R.drawable.ic_pin, R.string.recent_task_option_pin, target,
422                     taskContainer.getItemInfo(), taskContainer.getTaskView());
423             mTaskView = taskContainer.getTaskView();
424         }
425 
426         @Override
onClick(View view)427         public void onClick(View view) {
428             if (mTaskView.launchTaskAnimated() != null) {
429                 SystemUiProxy.INSTANCE.get(mTarget.asContext()).startScreenPinning(
430                         mTaskView.getFirstTask().key.id);
431             }
432             dismissTaskMenuView();
433             mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getFirstItemInfo())
434                     .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
435         }
436     }
437 
438     TaskShortcutFactory INSTALL = new TaskShortcutFactory() {
439         @Override
440         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
441                 TaskContainer taskContainer) {
442             Task t = taskContainer.getTask();
443             return InstantAppResolver.newInstance(container.asContext()).isInstantApp(
444                     t.getTopComponent().getPackageName(), t.getKey().userId)
445                     ? Collections.singletonList(new SystemShortcut.Install(container,
446                     taskContainer.getItemInfo(), taskContainer.getTaskView()))
447                     : null;
448         }
449     };
450 
451     TaskShortcutFactory WELLBEING = new TaskShortcutFactory() {
452         @Override
453         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
454                 TaskContainer taskContainer) {
455             SystemShortcut<ActivityContext> wellbeingShortcut =
456                     WellbeingModel.SHORTCUT_FACTORY.getShortcut(container,
457                             taskContainer.getItemInfo(), taskContainer.getTaskView());
458             return createSingletonShortcutList(wellbeingShortcut);
459         }
460     };
461 
462     TaskShortcutFactory SCREENSHOT = new TaskShortcutFactory() {
463         @Override
464         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
465                 TaskContainer taskContainer) {
466             boolean isTablet = container.getDeviceProfile().isTablet;
467             boolean isGridOnlyOverview = isTablet && Flags.enableGridOnlyOverview();
468             // Extra conditions if it's not grid-only overview
469             if (!isGridOnlyOverview) {
470                 RecentsOrientedState orientedState = taskContainer.getTaskView().getOrientedState();
471                 boolean isFakeLandscape = !orientedState.isRecentsActivityRotationAllowed()
472                         && orientedState.getTouchRotation() != ROTATION_0;
473                 if (!isFakeLandscape) {
474                     return null;
475                 }
476             }
477 
478             SystemShortcut screenshotShortcut = taskContainer.getOverlay().getScreenshotShortcut(
479                     container, taskContainer.getItemInfo(), taskContainer.getTaskView());
480             return createSingletonShortcutList(screenshotShortcut);
481         }
482 
483         @Override
484         public boolean showForDesktopTask() {
485             return true;
486         }
487     };
488 
489     TaskShortcutFactory MODAL = new TaskShortcutFactory() {
490         @Override
491         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
492                 TaskContainer taskContainer) {
493             boolean isTablet = container.getDeviceProfile().isTablet;
494             boolean isGridOnlyOverview = isTablet && Flags.enableGridOnlyOverview();
495             if (!isGridOnlyOverview) {
496                 return null;
497             }
498 
499             SystemShortcut modalStateSystemShortcut =
500                     taskContainer.getOverlay().getModalStateSystemShortcut(
501                             taskContainer.getItemInfo(), taskContainer.getTaskView());
502             return createSingletonShortcutList(modalStateSystemShortcut);
503         }
504     };
505 }
506