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