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.touch; 17 18 import static com.android.launcher3.Launcher.REQUEST_BIND_PENDING_APPWIDGET; 19 import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN; 21 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER; 22 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER; 23 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER; 24 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE; 25 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED; 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.AlertDialog; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.LauncherApps; 33 import android.content.pm.PackageInstaller.SessionInfo; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.view.View; 37 import android.view.View.OnClickListener; 38 import android.widget.Toast; 39 40 import com.android.launcher3.BubbleTextView; 41 import com.android.launcher3.Launcher; 42 import com.android.launcher3.LauncherSettings; 43 import com.android.launcher3.R; 44 import com.android.launcher3.Utilities; 45 import com.android.launcher3.apppairs.AppPairIcon; 46 import com.android.launcher3.folder.Folder; 47 import com.android.launcher3.folder.FolderIcon; 48 import com.android.launcher3.logging.InstanceId; 49 import com.android.launcher3.logging.InstanceIdSequence; 50 import com.android.launcher3.logging.StatsLogManager; 51 import com.android.launcher3.model.data.AppInfo; 52 import com.android.launcher3.model.data.FolderInfo; 53 import com.android.launcher3.model.data.ItemInfo; 54 import com.android.launcher3.model.data.ItemInfoWithIcon; 55 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 56 import com.android.launcher3.model.data.WorkspaceItemInfo; 57 import com.android.launcher3.pm.InstallSessionHelper; 58 import com.android.launcher3.shortcuts.ShortcutKey; 59 import com.android.launcher3.testing.TestLogging; 60 import com.android.launcher3.testing.shared.TestProtocol; 61 import com.android.launcher3.util.ItemInfoMatcher; 62 import com.android.launcher3.util.PackageManagerHelper; 63 import com.android.launcher3.views.FloatingIconView; 64 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 65 import com.android.launcher3.widget.PendingAppWidgetHostView; 66 import com.android.launcher3.widget.WidgetAddFlowHandler; 67 import com.android.launcher3.widget.WidgetManagerHelper; 68 69 import java.util.Collections; 70 import java.util.concurrent.CompletableFuture; 71 import java.util.function.Consumer; 72 73 /** 74 * Class for handling clicks on workspace and all-apps items 75 */ 76 public class ItemClickHandler { 77 78 private static final String TAG = ItemClickHandler.class.getSimpleName(); 79 80 /** 81 * Instance used for click handling on items 82 */ 83 public static final OnClickListener INSTANCE = ItemClickHandler::onClick; 84 onClick(View v)85 private static void onClick(View v) { 86 // Make sure that rogue clicks don't get through while allapps is launching, or after the 87 // view has detached (it's possible for this to happen if the view is removed mid touch). 88 if (v.getWindowToken() == null) return; 89 90 Launcher launcher = Launcher.getLauncher(v.getContext()); 91 if (!launcher.getWorkspace().isFinishedSwitchingState()) return; 92 93 Object tag = v.getTag(); 94 if (tag instanceof WorkspaceItemInfo) { 95 onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher); 96 } else if (tag instanceof FolderInfo) { 97 if (v instanceof FolderIcon) { 98 onClickFolderIcon(v); 99 } else if (v instanceof AppPairIcon) { 100 onClickAppPairIcon(v); 101 } 102 } else if (tag instanceof AppInfo) { 103 startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher); 104 } else if (tag instanceof LauncherAppWidgetInfo) { 105 if (v instanceof PendingAppWidgetHostView) { 106 onClickPendingWidget((PendingAppWidgetHostView) v, launcher); 107 } 108 } else if (tag instanceof ItemClickProxy) { 109 ((ItemClickProxy) tag).onItemClicked(v); 110 } 111 } 112 113 /** 114 * Event handler for a folder icon click. 115 * 116 * @param v The view that was clicked. Must be an instance of {@link FolderIcon}. 117 */ onClickFolderIcon(View v)118 private static void onClickFolderIcon(View v) { 119 Folder folder = ((FolderIcon) v).getFolder(); 120 if (!folder.isOpen() && !folder.isDestroyed()) { 121 // Open the requested folder 122 folder.animateOpen(); 123 StatsLogManager.newInstance(v.getContext()).logger().withItemInfo(folder.mInfo) 124 .log(LAUNCHER_FOLDER_OPEN); 125 } 126 } 127 128 /** 129 * Event handler for an app pair icon click. 130 * 131 * @param v The view that was clicked. Must be an instance of {@link AppPairIcon}. 132 */ onClickAppPairIcon(View v)133 private static void onClickAppPairIcon(View v) { 134 Launcher launcher = Launcher.getLauncher(v.getContext()); 135 FolderInfo folderInfo = ((AppPairIcon) v).getInfo(); 136 launcher.launchAppPair(folderInfo.contents.get(0), folderInfo.contents.get(1)); 137 } 138 139 /** 140 * Event handler for the app widget view which has not fully restored. 141 */ onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher)142 private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) { 143 if (launcher.getPackageManager().isSafeMode()) { 144 Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show(); 145 return; 146 } 147 148 final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); 149 if (v.isReadyForClickSetup()) { 150 LauncherAppWidgetProviderInfo appWidgetInfo = new WidgetManagerHelper(launcher) 151 .findProvider(info.providerName, info.user); 152 if (appWidgetInfo == null) { 153 return; 154 } 155 WidgetAddFlowHandler addFlowHandler = new WidgetAddFlowHandler(appWidgetInfo); 156 157 if (info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { 158 if (!info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) { 159 // This should not happen, as we make sure that an Id is allocated during bind. 160 return; 161 } 162 addFlowHandler.startBindFlow(launcher, info.appWidgetId, info, 163 REQUEST_BIND_PENDING_APPWIDGET); 164 } else { 165 addFlowHandler.startConfigActivity(launcher, info, REQUEST_RECONFIGURE_APPWIDGET); 166 } 167 } else { 168 final String packageName = info.providerName.getPackageName(); 169 onClickPendingAppItem(v, launcher, packageName, info.installProgress >= 0); 170 } 171 } 172 onClickPendingAppItem(View v, Launcher launcher, String packageName, boolean downloadStarted)173 private static void onClickPendingAppItem(View v, Launcher launcher, String packageName, 174 boolean downloadStarted) { 175 ItemInfo item = (ItemInfo) v.getTag(); 176 CompletableFuture<SessionInfo> siFuture; 177 if (Utilities.ATLEAST_Q) { 178 siFuture = CompletableFuture.supplyAsync(() -> 179 InstallSessionHelper.INSTANCE.get(launcher) 180 .getActiveSessionInfo(item.user, packageName), 181 UI_HELPER_EXECUTOR); 182 } else { 183 siFuture = CompletableFuture.completedFuture(null); 184 } 185 Consumer<SessionInfo> marketLaunchAction = sessionInfo -> { 186 if (sessionInfo != null && Utilities.ATLEAST_Q) { 187 LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class); 188 try { 189 launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null, 190 launcher.getActivityLaunchOptions(v, item).toBundle()); 191 return; 192 } catch (Exception e) { 193 Log.e(TAG, "Unable to launch market intent for package=" + packageName, e); 194 } 195 } 196 // Fallback to using custom market intent. 197 Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName); 198 launcher.startActivitySafely(v, intent, item); 199 }; 200 201 if (downloadStarted) { 202 // If the download has started, simply direct to the market app. 203 siFuture.thenAcceptAsync(marketLaunchAction, MAIN_EXECUTOR); 204 return; 205 } 206 new AlertDialog.Builder(launcher) 207 .setTitle(R.string.abandoned_promises_title) 208 .setMessage(R.string.abandoned_promise_explanation) 209 .setPositiveButton(R.string.abandoned_search, 210 (d, i) -> siFuture.thenAcceptAsync(marketLaunchAction, MAIN_EXECUTOR)) 211 .setNeutralButton(R.string.abandoned_clean_this, 212 (d, i) -> launcher.getWorkspace() 213 .persistRemoveItemsByMatcher(ItemInfoMatcher.ofPackages( 214 Collections.singleton(packageName), item.user), 215 "user explicitly removes the promise app icon")) 216 .create().show(); 217 } 218 219 /** 220 * Handles clicking on a disabled shortcut 221 * 222 * @return true iff the disabled item click has been handled. 223 */ handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context)224 public static boolean handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context) { 225 final int disabledFlags = shortcut.runtimeStatusFlags 226 & WorkspaceItemInfo.FLAG_DISABLED_MASK; 227 // Handle the case where the disabled reason is DISABLED_REASON_VERSION_LOWER. 228 // Show an AlertDialog for the user to choose either updating the app or cancel the launch. 229 if (maybeCreateAlertDialogForShortcut(shortcut, context)) { 230 return true; 231 } 232 233 if ((disabledFlags 234 & ~FLAG_DISABLED_SUSPENDED 235 & ~FLAG_DISABLED_QUIET_USER) == 0) { 236 // If the app is only disabled because of the above flags, launch activity anyway. 237 // Framework will tell the user why the app is suspended. 238 return false; 239 } else { 240 if (!TextUtils.isEmpty(shortcut.disabledMessage)) { 241 // Use a message specific to this shortcut, if it has one. 242 Toast.makeText(context, shortcut.disabledMessage, Toast.LENGTH_SHORT).show(); 243 return true; 244 } 245 // Otherwise just use a generic error message. 246 int error = R.string.activity_not_available; 247 if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) { 248 error = R.string.safemode_shortcut_error; 249 } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0 250 || (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) { 251 error = R.string.shortcut_not_available; 252 } 253 Toast.makeText(context, error, Toast.LENGTH_SHORT).show(); 254 return true; 255 } 256 } 257 maybeCreateAlertDialogForShortcut(final WorkspaceItemInfo shortcut, Context context)258 private static boolean maybeCreateAlertDialogForShortcut(final WorkspaceItemInfo shortcut, 259 Context context) { 260 try { 261 final Launcher launcher = Launcher.getLauncher(context); 262 if (shortcut.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT 263 && shortcut.isDisabledVersionLower()) { 264 final Intent marketIntent = shortcut.getMarketIntent(context); 265 // No market intent means no target package for the shortcut, which should be an 266 // issue. Falling back to showing toast messages. 267 if (marketIntent == null) { 268 return false; 269 } 270 271 new AlertDialog.Builder(context) 272 .setTitle(R.string.dialog_update_title) 273 .setMessage(R.string.dialog_update_message) 274 .setPositiveButton(R.string.dialog_update, (d, i) -> { 275 // Direct the user to the play store to update the app 276 context.startActivity(marketIntent); 277 }) 278 .setNeutralButton(R.string.dialog_remove, (d, i) -> { 279 // Remove the icon if launcher is successfully initialized 280 launcher.getWorkspace().persistRemoveItemsByMatcher(ItemInfoMatcher 281 .ofShortcutKeys(Collections.singleton(ShortcutKey 282 .fromItemInfo(shortcut))), 283 "user explicitly removes disabled shortcut"); 284 }) 285 .create() 286 .show(); 287 return true; 288 } 289 } catch (Exception e) { 290 Log.e(TAG, "Error creating alert dialog", e); 291 } 292 293 return false; 294 } 295 296 /** 297 * Event handler for an app shortcut click. 298 * 299 * @param v The view that was clicked. Must be a tagged with a {@link WorkspaceItemInfo}. 300 */ onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher)301 public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) { 302 if (shortcut.isDisabled() && handleDisabledItemClicked(shortcut, launcher)) { 303 return; 304 } 305 306 // Check for abandoned promise 307 if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) { 308 String packageName = shortcut.getIntent().getComponent() != null 309 ? shortcut.getIntent().getComponent().getPackageName() 310 : shortcut.getIntent().getPackage(); 311 if (!TextUtils.isEmpty(packageName)) { 312 onClickPendingAppItem( 313 v, 314 launcher, 315 packageName, 316 (shortcut.runtimeStatusFlags 317 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0); 318 return; 319 } 320 } 321 322 // Start activities 323 startAppShortcutOrInfoActivity(v, shortcut, launcher); 324 } 325 startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher)326 private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) { 327 TestLogging.recordEvent( 328 TestProtocol.SEQUENCE_MAIN, "start: startAppShortcutOrInfoActivity"); 329 Intent intent; 330 if (item instanceof ItemInfoWithIcon 331 && (((ItemInfoWithIcon) item).runtimeStatusFlags 332 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) { 333 ItemInfoWithIcon appInfo = (ItemInfoWithIcon) item; 334 intent = new PackageManagerHelper(launcher) 335 .getMarketIntent(appInfo.getTargetComponent().getPackageName()); 336 } else { 337 intent = item.getIntent(); 338 } 339 if (intent == null) { 340 throw new IllegalArgumentException("Input must have a valid intent"); 341 } 342 if (item instanceof WorkspaceItemInfo) { 343 WorkspaceItemInfo si = (WorkspaceItemInfo) item; 344 if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) 345 && Intent.ACTION_VIEW.equals(intent.getAction())) { 346 // make a copy of the intent that has the package set to null 347 // we do this because the platform sometimes disables instant 348 // apps temporarily (triggered by the user) and fallbacks to the 349 // web ui. This only works though if the package isn't set 350 intent = new Intent(intent); 351 intent.setPackage(null); 352 } 353 if ((si.options & WorkspaceItemInfo.FLAG_START_FOR_RESULT) != 0) { 354 launcher.startActivityForResult(item.getIntent(), 0); 355 InstanceId instanceId = new InstanceIdSequence().newInstanceId(); 356 launcher.logAppLaunch(launcher.getStatsLogManager(), item, instanceId); 357 return; 358 } 359 } 360 if (v != null && launcher.supportsAdaptiveIconAnimation(v) 361 && !item.shouldUseBackgroundAnimation()) { 362 // Preload the icon to reduce latency b/w swapping the floating view with the original. 363 FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */); 364 } 365 launcher.startActivitySafely(v, intent, item); 366 } 367 368 /** 369 * Interface to indicate that an item will handle the click itself. 370 */ 371 public interface ItemClickProxy { 372 373 /** 374 * Called when the item is clicked 375 */ onItemClicked(View view)376 void onItemClicked(View view); 377 } 378 } 379