• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
22 
23 import android.app.Activity;
24 import android.app.ActivityOptions;
25 import android.graphics.Bitmap;
26 import android.graphics.Color;
27 import android.graphics.Rect;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.RemoteException;
31 import android.os.SystemProperties;
32 import android.util.Log;
33 import android.view.View;
34 import android.view.WindowInsets;
35 import android.view.WindowManagerGlobal;
36 import android.window.SplashScreen;
37 
38 import androidx.annotation.Nullable;
39 
40 import com.android.launcher3.BaseDraggingActivity;
41 import com.android.launcher3.DeviceProfile;
42 import com.android.launcher3.R;
43 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
44 import com.android.launcher3.model.WellbeingModel;
45 import com.android.launcher3.popup.SystemShortcut;
46 import com.android.launcher3.popup.SystemShortcut.AppInfo;
47 import com.android.launcher3.touch.PagedOrientationHandler;
48 import com.android.launcher3.util.InstantAppResolver;
49 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
50 import com.android.quickstep.views.RecentsView;
51 import com.android.quickstep.views.TaskThumbnailView;
52 import com.android.quickstep.views.TaskView;
53 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
54 import com.android.systemui.shared.recents.model.Task;
55 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
56 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
57 import com.android.systemui.shared.recents.view.RecentsTransition;
58 import com.android.systemui.shared.system.ActivityManagerWrapper;
59 
60 import java.util.Collections;
61 import java.util.List;
62 import java.util.function.Function;
63 import java.util.stream.Collectors;
64 
65 /**
66  * Represents a system shortcut that can be shown for a recent task.
67  */
68 public interface TaskShortcutFactory {
69     @Nullable
getShortcuts(BaseDraggingActivity activity, TaskIdAttributeContainer taskContainer)70     default List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
71             TaskIdAttributeContainer taskContainer) {
72         return null;
73     }
74 
showForSplitscreen()75     default boolean showForSplitscreen() {
76         return false;
77     }
78 
79     /** @return a singleton list if the provided shortcut is non-null, null otherwise */
80     @Nullable
createSingletonShortcutList(@ullable SystemShortcut shortcut)81     default List<SystemShortcut> createSingletonShortcutList(@Nullable SystemShortcut shortcut) {
82         if (shortcut != null) {
83             return Collections.singletonList(shortcut);
84         }
85         return null;
86     }
87 
88     TaskShortcutFactory APP_INFO = new TaskShortcutFactory() {
89         @Override
90         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
91                 TaskIdAttributeContainer taskContainer) {
92             TaskView taskView = taskContainer.getTaskView();
93             AppInfo.SplitAccessibilityInfo accessibilityInfo =
94                     new AppInfo.SplitAccessibilityInfo(taskView.containsMultipleTasks(),
95                             TaskUtils.getTitle(taskView.getContext(), taskContainer.getTask()),
96                             taskContainer.getA11yNodeId()
97                     );
98             return Collections.singletonList(new AppInfo(activity, taskContainer.getItemInfo(),
99                     taskView, accessibilityInfo));
100         }
101 
102         @Override
103         public boolean showForSplitscreen() {
104             return true;
105         }
106     };
107 
108     class SplitSelectSystemShortcut extends SystemShortcut {
109         private final TaskView mTaskView;
110         private final SplitPositionOption mSplitPositionOption;
111 
SplitSelectSystemShortcut(BaseDraggingActivity target, TaskView taskView, SplitPositionOption option)112         public SplitSelectSystemShortcut(BaseDraggingActivity target, TaskView taskView,
113                 SplitPositionOption option) {
114             super(option.iconResId, option.textResId, target, taskView.getItemInfo(), taskView);
115             mTaskView = taskView;
116             mSplitPositionOption = option;
117         }
118 
119         @Override
onClick(View view)120         public void onClick(View view) {
121             mTaskView.initiateSplitSelect(mSplitPositionOption);
122         }
123     }
124 
125     class FreeformSystemShortcut extends SystemShortcut<BaseDraggingActivity> {
126         private static final String TAG = "FreeformSystemShortcut";
127 
128         private Handler mHandler;
129 
130         private final RecentsView mRecentsView;
131         private final TaskThumbnailView mThumbnailView;
132         private final TaskView mTaskView;
133         private final LauncherEvent mLauncherEvent;
134 
FreeformSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity, TaskIdAttributeContainer taskContainer, LauncherEvent launcherEvent)135         public FreeformSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity,
136                 TaskIdAttributeContainer taskContainer, LauncherEvent launcherEvent) {
137             super(iconRes, textRes, activity, taskContainer.getItemInfo(),
138                     taskContainer.getTaskView());
139             mLauncherEvent = launcherEvent;
140             mHandler = new Handler(Looper.getMainLooper());
141             mTaskView = taskContainer.getTaskView();
142             mRecentsView = activity.getOverviewPanel();
143             mThumbnailView = taskContainer.getThumbnailView();
144         }
145 
146         @Override
onClick(View view)147         public void onClick(View view) {
148             dismissTaskMenuView(mTarget);
149             RecentsView rv = mTarget.getOverviewPanel();
150             rv.switchToScreenshot(() -> {
151                 rv.finishRecentsAnimation(true /* toHome */, () -> {
152                     mTarget.returnToHomescreen();
153                     rv.getHandler().post(this::startActivity);
154                 });
155             });
156         }
157 
startActivity()158         private void startActivity() {
159             final Task.TaskKey taskKey = mTaskView.getTask().key;
160             final int taskId = taskKey.id;
161             final ActivityOptions options = makeLaunchOptions(mTarget);
162             if (options != null) {
163                 options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
164             }
165             if (options != null
166                     && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
167                             options)) {
168                 final Runnable animStartedListener = () -> {
169                     // Hide the task view and wait for the window to be resized
170                     // TODO: Consider animating in launcher and do an in-place start activity
171                     //       afterwards
172                     mRecentsView.setIgnoreResetTask(taskId);
173                     mTaskView.setAlpha(0f);
174                 };
175 
176                 final int[] position = new int[2];
177                 mThumbnailView.getLocationOnScreen(position);
178                 final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
179                 final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
180                 final Rect taskBounds = new Rect(position[0], position[1],
181                         position[0] + width, position[1] + height);
182 
183                 // Take the thumbnail of the task without a scrim and apply it back after
184                 float alpha = mThumbnailView.getDimAlpha();
185                 mThumbnailView.setDimAlpha(0);
186                 Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
187                         taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
188                         Color.BLACK);
189                 mThumbnailView.setDimAlpha(alpha);
190 
191                 AppTransitionAnimationSpecsFuture future =
192                         new AppTransitionAnimationSpecsFuture(mHandler) {
193                     @Override
194                     public List<AppTransitionAnimationSpecCompat> composeSpecs() {
195                         return Collections.singletonList(new AppTransitionAnimationSpecCompat(
196                                 taskId, thumbnail, taskBounds));
197                     }
198                 };
199                 overridePendingAppTransitionMultiThumbFuture(
200                         future, animStartedListener, mHandler, true /* scaleUp */,
201                         taskKey.displayId);
202                 mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
203                         .log(mLauncherEvent);
204             }
205         }
206 
207         /**
208          * Overrides a pending app transition.
209          */
overridePendingAppTransitionMultiThumbFuture( AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback, Handler animStartedCallbackHandler, boolean scaleUp, int displayId)210         private void overridePendingAppTransitionMultiThumbFuture(
211                 AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback,
212                 Handler animStartedCallbackHandler, boolean scaleUp, int displayId) {
213             try {
214                 WindowManagerGlobal.getWindowManagerService()
215                         .overridePendingAppTransitionMultiThumbFuture(
216                                 animationSpecFuture.getFuture(),
217                                 RecentsTransition.wrapStartedListener(animStartedCallbackHandler,
218                                         animStartedCallback), scaleUp, displayId);
219             } catch (RemoteException e) {
220                 Log.w(TAG, "Failed to override pending app transition (multi-thumbnail future): ",
221                         e);
222             }
223         }
224 
makeLaunchOptions(Activity activity)225         private ActivityOptions makeLaunchOptions(Activity activity) {
226             ActivityOptions activityOptions = ActivityOptions.makeBasic();
227             activityOptions.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
228             // Arbitrary bounds only because freeform is in dev mode right now
229             final View decorView = activity.getWindow().getDecorView();
230             final WindowInsets insets = decorView.getRootWindowInsets();
231             final Rect r = new Rect(0, 0, decorView.getWidth() / 2, decorView.getHeight() / 2);
232             r.offsetTo(insets.getSystemWindowInsetLeft() + 50,
233                     insets.getSystemWindowInsetTop() + 50);
234             activityOptions.setLaunchBounds(r);
235             return activityOptions;
236         }
237     }
238 
239     /**
240      * Does NOT add split options in the following scenarios:
241      * * The taskView to add split options is already showing split screen tasks
242      * * There aren't at least 2 tasks in overview to show split options for
243      * * Split isn't supported by the task itself (non resizable activity)
244      * * We aren't currently in multi-window
245      * * The taskView to show split options for is the focused task AND we haven't started
246      * scrolling in overview (if we haven't scrolled, there's a split overview action button so
247      * we don't need this menu option)
248      */
249     TaskShortcutFactory SPLIT_SELECT = new TaskShortcutFactory() {
250         @Override
251         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
252                 TaskIdAttributeContainer taskContainer) {
253             DeviceProfile deviceProfile = activity.getDeviceProfile();
254             final Task task  = taskContainer.getTask();
255             final TaskView taskView = taskContainer.getTaskView();
256             final RecentsView recentsView = taskView.getRecentsView();
257             final PagedOrientationHandler orientationHandler =
258                     recentsView.getPagedOrientationHandler();
259 
260             int[] taskViewTaskIds = taskView.getTaskIds();
261             boolean taskViewHasMultipleTasks = taskViewTaskIds[0] != -1 &&
262                     taskViewTaskIds[1] != -1;
263             boolean notEnoughTasksToSplit = recentsView.getTaskViewCount() < 2;
264             boolean isFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
265             boolean isTaskInExpectedScrollPosition =
266                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
267             boolean isTaskSplitNotSupported = !task.isDockable;
268             boolean hideForExistingMultiWindow = activity.getDeviceProfile().isMultiWindowMode;
269 
270             if (taskViewHasMultipleTasks ||
271                     notEnoughTasksToSplit ||
272                     isTaskSplitNotSupported ||
273                     hideForExistingMultiWindow ||
274                     (isFocusedTask && isTaskInExpectedScrollPosition)) {
275                 return null;
276             }
277 
278             return orientationHandler.getSplitPositionOptions(deviceProfile)
279                     .stream()
280                     .map((Function<SplitPositionOption, SystemShortcut>) option ->
281                             new SplitSelectSystemShortcut(activity, taskView, option))
282                     .collect(Collectors.toList());
283         }
284     };
285 
286     TaskShortcutFactory FREE_FORM = new TaskShortcutFactory() {
287         @Override
288         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
289                 TaskIdAttributeContainer taskContainer) {
290             final Task task  = taskContainer.getTask();
291             if (!task.isDockable) {
292                 return null;
293             }
294             if (!isAvailable(activity, task.key.displayId)) {
295                 return null;
296             }
297 
298             return Collections.singletonList(new FreeformSystemShortcut(
299                     R.drawable.ic_caption_desktop_button_foreground,
300                     R.string.recent_task_option_freeform, activity, taskContainer,
301                     LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP));
302         }
303 
304         private boolean isAvailable(BaseDraggingActivity activity, int displayId) {
305             return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity)
306                     && !SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false)
307                     && !SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false);
308         }
309     };
310 
311     TaskShortcutFactory PIN = new TaskShortcutFactory() {
312         @Override
313         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
314                 TaskIdAttributeContainer taskContainer) {
315             if (!SystemUiProxy.INSTANCE.get(activity).isActive()) {
316                 return null;
317             }
318             if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
319                 return null;
320             }
321             if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
322                 // We shouldn't be able to pin while an app is locked.
323                 return null;
324             }
325             return Collections.singletonList(new PinSystemShortcut(activity, taskContainer));
326         }
327     };
328 
329     class PinSystemShortcut extends SystemShortcut<BaseDraggingActivity> {
330 
331         private static final String TAG = "PinSystemShortcut";
332 
333         private final TaskView mTaskView;
334 
PinSystemShortcut(BaseDraggingActivity target, TaskIdAttributeContainer taskContainer)335         public PinSystemShortcut(BaseDraggingActivity target,
336                 TaskIdAttributeContainer taskContainer) {
337             super(R.drawable.ic_pin, R.string.recent_task_option_pin, target,
338                     taskContainer.getItemInfo(), taskContainer.getTaskView());
339             mTaskView = taskContainer.getTaskView();
340         }
341 
342         @Override
onClick(View view)343         public void onClick(View view) {
344             if (mTaskView.launchTaskAnimated() != null) {
345                 SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(mTaskView.getTask().key.id);
346             }
347             dismissTaskMenuView(mTarget);
348             mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
349                     .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
350         }
351     }
352 
353     TaskShortcutFactory INSTALL = new TaskShortcutFactory() {
354         @Override
355         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
356                 TaskIdAttributeContainer taskContainer) {
357             return InstantAppResolver.newInstance(activity).isInstantApp(activity,
358                     taskContainer.getTask().getTopComponent().getPackageName()) ?
359                     Collections.singletonList(new SystemShortcut.Install(activity,
360                             taskContainer.getItemInfo(), taskContainer.getTaskView())) :
361                     null;
362         }
363     };
364 
365     TaskShortcutFactory WELLBEING = new TaskShortcutFactory() {
366         @Override
367         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
368                 TaskIdAttributeContainer taskContainer) {
369             SystemShortcut<BaseDraggingActivity> wellbeingShortcut =
370                     WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity,
371                             taskContainer.getItemInfo(), taskContainer.getTaskView());
372             return createSingletonShortcutList(wellbeingShortcut);
373         }
374     };
375 
376     TaskShortcutFactory SCREENSHOT = new TaskShortcutFactory() {
377         @Override
378         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
379                 TaskIdAttributeContainer taskContainer) {
380             SystemShortcut screenshotShortcut = taskContainer.getThumbnailView().getTaskOverlay()
381                     .getScreenshotShortcut(activity, taskContainer.getItemInfo(),
382                             taskContainer.getTaskView());
383             return createSingletonShortcutList(screenshotShortcut);
384         }
385     };
386 
387     TaskShortcutFactory MODAL = new TaskShortcutFactory() {
388         @Override
389         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
390                 TaskIdAttributeContainer taskContainer) {
391             SystemShortcut modalStateSystemShortcut =
392                     taskContainer.getThumbnailView().getTaskOverlay()
393                             .getModalStateSystemShortcut(
394                                     taskContainer.getItemInfo(), taskContainer.getTaskView());
395             return createSingletonShortcutList(modalStateSystemShortcut);
396         }
397     };
398 }
399