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