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