• 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 package com.android.launcher3.views;
17 
18 import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR;
19 
20 import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON;
21 import static com.android.launcher3.Utilities.allowBGLaunch;
22 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_PENDING_INTENT;
25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
26 import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
27 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
28 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
29 
30 import android.app.ActivityOptions;
31 import android.app.PendingIntent;
32 import android.content.ActivityNotFoundException;
33 import android.content.Context;
34 import android.content.ContextWrapper;
35 import android.content.Intent;
36 import android.content.pm.LauncherApps;
37 import android.graphics.Rect;
38 import android.graphics.drawable.Drawable;
39 import android.os.Bundle;
40 import android.os.IBinder;
41 import android.os.Process;
42 import android.os.UserHandle;
43 import android.util.Log;
44 import android.view.Display;
45 import android.view.LayoutInflater;
46 import android.view.View;
47 import android.view.View.AccessibilityDelegate;
48 import android.view.WindowInsets;
49 import android.view.WindowInsetsController;
50 import android.view.inputmethod.InputMethodManager;
51 import android.widget.Toast;
52 
53 import androidx.annotation.NonNull;
54 import androidx.annotation.Nullable;
55 
56 import com.android.launcher3.BubbleTextView;
57 import com.android.launcher3.DeviceProfile;
58 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
59 import com.android.launcher3.DropTargetHandler;
60 import com.android.launcher3.LauncherSettings;
61 import com.android.launcher3.R;
62 import com.android.launcher3.Utilities;
63 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
64 import com.android.launcher3.celllayout.CellPosMapper;
65 import com.android.launcher3.dot.DotInfo;
66 import com.android.launcher3.dragndrop.DragController;
67 import com.android.launcher3.folder.FolderIcon;
68 import com.android.launcher3.logger.LauncherAtom;
69 import com.android.launcher3.logging.InstanceId;
70 import com.android.launcher3.logging.InstanceIdSequence;
71 import com.android.launcher3.logging.StatsLogManager;
72 import com.android.launcher3.model.StringCache;
73 import com.android.launcher3.model.data.ItemInfo;
74 import com.android.launcher3.model.data.WorkspaceItemInfo;
75 import com.android.launcher3.popup.PopupDataProvider;
76 import com.android.launcher3.util.ActivityOptionsWrapper;
77 import com.android.launcher3.util.OnboardingPrefs;
78 import com.android.launcher3.util.PackageManagerHelper;
79 import com.android.launcher3.util.Preconditions;
80 import com.android.launcher3.util.RunnableList;
81 import com.android.launcher3.util.SplitConfigurationOptions;
82 import com.android.launcher3.util.ViewCache;
83 
84 import java.util.List;
85 
86 /**
87  * An interface to be used along with a context for various activities in Launcher. This allows a
88  * generic class to depend on Context subclass instead of an Activity.
89  */
90 public interface ActivityContext {
91 
92     String TAG = "ActivityContext";
93 
finishAutoCancelActionMode()94     default boolean finishAutoCancelActionMode() {
95         return false;
96     }
97 
getDotInfoForItem(ItemInfo info)98     default DotInfo getDotInfoForItem(ItemInfo info) {
99         return null;
100     }
101 
102     /**
103      * For items with tree hierarchy, notifies the activity to invalidate the parent when a root
104      * is invalidated
105      * @param info info associated with a root node.
106      */
invalidateParent(ItemInfo info)107     default void invalidateParent(ItemInfo info) { }
108 
getAccessibilityDelegate()109     default AccessibilityDelegate getAccessibilityDelegate() {
110         return null;
111     }
112 
getFolderBoundingBox()113     default Rect getFolderBoundingBox() {
114         return getDeviceProfile().getAbsoluteOpenFolderBounds();
115     }
116 
117     /**
118      * After calling {@link #getFolderBoundingBox()}, we calculate a (left, top) position for a
119      * Folder of size width x height to be within those bounds. However, the chosen position may
120      * not be visually ideal (e.g. uncanny valley of centeredness), so here's a chance to update it.
121      * @param inOutPosition A 2-size array where the first element is the left position of the open
122      *     folder and the second element is the top position. Should be updated in place if desired.
123      * @param bounds The bounds that the open folder should fit inside.
124      * @param width The width of the open folder.
125      * @param height The height of the open folder.
126      */
updateOpenFolderPosition(int[] inOutPosition, Rect bounds, int width, int height)127     default void updateOpenFolderPosition(int[] inOutPosition, Rect bounds, int width, int height) {
128     }
129 
130     /**
131      * Returns a LayoutInflater that is cloned in this Context, so that Views inflated by it will
132      * have the same Context. (i.e. {@link #lookupContext(Context)} will find this ActivityContext.)
133      */
getLayoutInflater()134     default LayoutInflater getLayoutInflater() {
135         if (this instanceof Context) {
136             Context context = (Context) this;
137             return LayoutInflater.from(context).cloneInContext(context);
138         }
139         return null;
140     }
141 
142     /** Called when the first app in split screen has been selected */
startSplitSelection( SplitConfigurationOptions.SplitSelectSource splitSelectSource)143     default void startSplitSelection(
144             SplitConfigurationOptions.SplitSelectSource splitSelectSource) {
145         // Overridden, intentionally empty
146     }
147 
148     /**
149      * The root view to support drag-and-drop and popup support.
150      */
getDragLayer()151     BaseDragLayer getDragLayer();
152 
153     /**
154      * The all apps container, if it exists in this context.
155      */
getAppsView()156     default ActivityAllAppsContainerView<?> getAppsView() {
157         return null;
158     }
159 
getDeviceProfile()160     DeviceProfile getDeviceProfile();
161 
162     /** Registered {@link OnDeviceProfileChangeListener} instances. */
getOnDeviceProfileChangeListeners()163     List<OnDeviceProfileChangeListener> getOnDeviceProfileChangeListeners();
164 
165     /** Notifies listeners of a {@link DeviceProfile} change. */
dispatchDeviceProfileChanged()166     default void dispatchDeviceProfileChanged() {
167         DeviceProfile deviceProfile = getDeviceProfile();
168         List<OnDeviceProfileChangeListener> listeners = getOnDeviceProfileChangeListeners();
169         for (int i = listeners.size() - 1; i >= 0; i--) {
170             listeners.get(i).onDeviceProfileChanged(deviceProfile);
171         }
172     }
173 
174     /** Register listener for {@link DeviceProfile} changes. */
addOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener)175     default void addOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener) {
176         getOnDeviceProfileChangeListeners().add(listener);
177     }
178 
179     /** Unregister listener for {@link DeviceProfile} changes. */
removeOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener)180     default void removeOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener) {
181         getOnDeviceProfileChangeListeners().remove(listener);
182     }
183 
getViewCache()184     default ViewCache getViewCache() {
185         return new ViewCache();
186     }
187 
188     /**
189      * Controller for supporting item drag-and-drop
190      */
getDragController()191     default <T extends DragController> T getDragController() {
192         return null;
193     }
194 
195     /**
196      * Handler for actions taken on drop targets that require launcher
197      */
getDropTargetHandler()198     default DropTargetHandler getDropTargetHandler() {
199         return null;
200     }
201 
202     /**
203      * Returns the FolderIcon with the given item id, if it exists.
204      */
findFolderIcon(final int folderIconId)205     default @Nullable FolderIcon findFolderIcon(final int folderIconId) {
206         return null;
207     }
208 
getStatsLogManager()209     default StatsLogManager getStatsLogManager() {
210         return StatsLogManager.newInstance((Context) this);
211     }
212 
213     /**
214      * Returns {@code true} if popups can use a range of color shades instead of a singular color.
215      */
canUseMultipleShadesForPopup()216     default boolean canUseMultipleShadesForPopup() {
217         return true;
218     }
219 
220     /**
221      * Called just before logging the given item.
222      */
applyOverwritesToLogItem(LauncherAtom.ItemInfo.Builder itemInfoBuilder)223     default void applyOverwritesToLogItem(LauncherAtom.ItemInfo.Builder itemInfoBuilder) { }
224 
225     /** Onboarding preferences for any onboarding data within this context. */
226     @Nullable
getOnboardingPrefs()227     default OnboardingPrefs<?> getOnboardingPrefs() {
228         return null;
229     }
230 
231     /** Returns {@code true} if items are currently being bound within this context. */
isBindingItems()232     default boolean isBindingItems() {
233         return false;
234     }
235 
getItemOnClickListener()236     default View.OnClickListener getItemOnClickListener() {
237         return v -> {
238             // No op.
239         };
240     }
241 
242     /** Long-click callback used for All Apps items. */
getAllAppsItemLongClickListener()243     default View.OnLongClickListener getAllAppsItemLongClickListener() {
244         return v -> false;
245     }
246 
247     @Nullable
getPopupDataProvider()248     default PopupDataProvider getPopupDataProvider() {
249         return null;
250     }
251 
252     @Nullable
getStringCache()253     default StringCache getStringCache() {
254         return null;
255     }
256 
257     /**
258      * Hides the keyboard if it is visible
259      */
hideKeyboard()260     default void hideKeyboard() {
261         View root = getDragLayer();
262         if (root == null) {
263             return;
264         }
265         if (Utilities.ATLEAST_R) {
266             Preconditions.assertUIThread();
267             //  Hide keyboard with WindowInsetsController if could. In case
268             //  hideSoftInputFromWindow may get ignored by input connection being finished
269             //  when the screen is off.
270             //
271             // In addition, inside IMF, the keyboards are closed asynchronously that launcher no
272             // longer need to post to the message queue.
273             final WindowInsetsController wic = root.getWindowInsetsController();
274             WindowInsets insets = root.getRootWindowInsets();
275             boolean isImeShown = insets != null && insets.isVisible(WindowInsets.Type.ime());
276             if (wic != null && isImeShown) {
277                 StatsLogManager slm  = getStatsLogManager();
278                 slm.keyboardStateManager().setKeyboardState(HIDE);
279 
280                 // this method cannot be called cross threads
281                 wic.hide(WindowInsets.Type.ime());
282                 slm.logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED);
283                 return;
284             }
285         }
286 
287         InputMethodManager imm = root.getContext().getSystemService(InputMethodManager.class);
288         IBinder token = root.getWindowToken();
289         if (imm != null && token != null) {
290             UI_HELPER_EXECUTOR.execute(() -> {
291                 if (imm.hideSoftInputFromWindow(token, 0)) {
292                     // log keyboard close event only when keyboard is actually closed
293                     MAIN_EXECUTOR.execute(() ->
294                             getStatsLogManager().logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED));
295                 }
296             });
297         }
298     }
299 
300     /**
301      * Sends a pending intent animating from a view.
302      *
303      * @param v View to animate.
304      * @param intent The pending intent being launched.
305      * @param item Item associated with the view.
306      * @return RunnableList for listening for animation finish if the activity was properly
307      *         or started, {@code null} if the launch finished
308      */
sendPendingIntentWithAnimation( @onNull View v, PendingIntent intent, @Nullable ItemInfo item)309     default RunnableList sendPendingIntentWithAnimation(
310             @NonNull View v, PendingIntent intent, @Nullable ItemInfo item) {
311         ActivityOptionsWrapper options = getActivityLaunchOptions(v, item);
312         try {
313             intent.send(null, 0, null, null, null, null, options.toBundle());
314             if (item != null) {
315                 InstanceId instanceId = new InstanceIdSequence().newInstanceId();
316                 getStatsLogManager().logger().withItemInfo(item).withInstanceId(instanceId)
317                         .log(LAUNCHER_APP_LAUNCH_PENDING_INTENT);
318             }
319             return options.onEndCallback;
320         } catch (PendingIntent.CanceledException e) {
321             Toast.makeText(v.getContext(),
322                     v.getContext().getResources().getText(R.string.shortcut_not_available),
323                     Toast.LENGTH_SHORT).show();
324         }
325         return null;
326     }
327 
328     /**
329      * Safely starts an activity.
330      *
331      * @param v View starting the activity.
332      * @param intent Base intent being launched.
333      * @param item Item associated with the view.
334      * @return RunnableList for listening for animation finish if the activity was properly
335      *         or started, {@code null} if the launch finished
336      */
startActivitySafely( View v, Intent intent, @Nullable ItemInfo item)337     default RunnableList startActivitySafely(
338             View v, Intent intent, @Nullable ItemInfo item) {
339         Preconditions.assertUIThread();
340         Context context = (Context) this;
341         if (isAppBlockedForSafeMode() && !PackageManagerHelper.isSystemApp(context, intent)) {
342             Toast.makeText(context, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
343             return null;
344         }
345 
346         boolean isShortcut = (item instanceof WorkspaceItemInfo)
347                 && item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
348                 && !((WorkspaceItemInfo) item).isPromise();
349         if (isShortcut && GO_DISABLE_WIDGETS) {
350             return null;
351         }
352         ActivityOptionsWrapper options = v != null ? getActivityLaunchOptions(v, item)
353                 : makeDefaultActivityOptions(item != null && item.animationType == DEFAULT_NO_ICON
354                         ? SPLASH_SCREEN_STYLE_SOLID_COLOR : -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */);
355         UserHandle user = item == null ? null : item.user;
356         Bundle optsBundle = options.toBundle();
357         // Prepare intent
358         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
359         if (v != null) {
360             intent.setSourceBounds(Utilities.getViewBounds(v));
361         }
362         try {
363             if (isShortcut) {
364                 String id = ((WorkspaceItemInfo) item).getDeepShortcutId();
365                 String packageName = intent.getPackage();
366                 ((Context) this).getSystemService(LauncherApps.class).startShortcut(
367                         packageName, id, intent.getSourceBounds(), optsBundle, user);
368             } else if (user == null || user.equals(Process.myUserHandle())) {
369                 // Could be launching some bookkeeping activity
370                 context.startActivity(intent, optsBundle);
371             } else {
372                 context.getSystemService(LauncherApps.class).startMainActivity(
373                         intent.getComponent(), user, intent.getSourceBounds(), optsBundle);
374             }
375             if (item != null) {
376                 InstanceId instanceId = new InstanceIdSequence().newInstanceId();
377                 logAppLaunch(getStatsLogManager(), item, instanceId);
378             }
379             return options.onEndCallback;
380         } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
381             Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
382             Log.e(TAG, "Unable to launch. tag=" + item + " intent=" + intent, e);
383         }
384         return null;
385     }
386 
387     /** Returns {@code true} if an app launch is blocked due to safe mode. */
isAppBlockedForSafeMode()388     default boolean isAppBlockedForSafeMode() {
389         return false;
390     }
391 
392     /**
393      * Creates and logs a new app launch event.
394      */
logAppLaunch(StatsLogManager statsLogManager, ItemInfo info, InstanceId instanceId)395     default void logAppLaunch(StatsLogManager statsLogManager, ItemInfo info,
396             InstanceId instanceId) {
397         statsLogManager.logger().withItemInfo(info).withInstanceId(instanceId)
398                 .log(LAUNCHER_APP_LAUNCH_TAP);
399     }
400 
401     /**
402      * Returns launch options for an Activity.
403      *
404      * @param v View initiating a launch.
405      * @param item Item associated with the view.
406      */
getActivityLaunchOptions(View v, @Nullable ItemInfo item)407     default ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
408         int left = 0, top = 0;
409         int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
410         if (v instanceof BubbleTextView) {
411             // Launch from center of icon, not entire view
412             Drawable icon = ((BubbleTextView) v).getIcon();
413             if (icon != null) {
414                 Rect bounds = icon.getBounds();
415                 left = (width - bounds.width()) / 2;
416                 top = v.getPaddingTop();
417                 width = bounds.width();
418                 height = bounds.height();
419             }
420         }
421         ActivityOptions options =
422                 allowBGLaunch(ActivityOptions.makeClipRevealAnimation(v, left, top, width, height));
423         options.setLaunchDisplayId(
424                 (v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
425                         : Display.DEFAULT_DISPLAY);
426         RunnableList callback = new RunnableList();
427         return new ActivityOptionsWrapper(options, callback);
428     }
429 
430     /**
431      * Creates a default activity option and we do not want association with any launcher element.
432      */
makeDefaultActivityOptions(int splashScreenStyle)433     default ActivityOptionsWrapper makeDefaultActivityOptions(int splashScreenStyle) {
434         ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
435         if (Utilities.ATLEAST_T) {
436             options.setSplashScreenStyle(splashScreenStyle);
437         }
438         return new ActivityOptionsWrapper(options, new RunnableList());
439     }
440 
getCellPosMapper()441     default CellPosMapper getCellPosMapper() {
442         return CellPosMapper.DEFAULT;
443     }
444 
445     /**
446      * Returns the ActivityContext associated with the given Context, or throws an exception if
447      * the Context is not associated with any ActivityContext.
448      */
lookupContext(Context context)449     static <T extends Context & ActivityContext> T lookupContext(Context context) {
450         T activityContext = lookupContextNoThrow(context);
451         if (activityContext == null) {
452             throw new IllegalArgumentException("Cannot find ActivityContext in parent tree");
453         }
454         return activityContext;
455     }
456 
457     /**
458      * Returns the ActivityContext associated with the given Context, or null if
459      * the Context is not associated with any ActivityContext.
460      */
lookupContextNoThrow(Context context)461     static <T extends Context & ActivityContext> T lookupContextNoThrow(Context context) {
462         if (context instanceof ActivityContext) {
463             return (T) context;
464         } else if (context instanceof ContextWrapper) {
465             return lookupContextNoThrow(((ContextWrapper) context).getBaseContext());
466         } else {
467             return null;
468         }
469     }
470 }
471