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