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