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.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER; 19 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER; 20 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER; 21 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE; 22 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED; 23 import static com.android.launcher3.Launcher.REQUEST_BIND_PENDING_APPWIDGET; 24 import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET; 25 import static com.android.launcher3.model.AppLaunchTracker.CONTAINER_ALL_APPS; 26 27 import android.app.AlertDialog; 28 import android.content.ActivityNotFoundException; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.LauncherApps; 32 import android.content.pm.PackageInstaller.SessionInfo; 33 import android.os.Process; 34 import android.os.UserHandle; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.view.View; 38 import android.view.View.OnClickListener; 39 import android.widget.Toast; 40 41 import androidx.annotation.Nullable; 42 43 import com.android.launcher3.AppInfo; 44 import com.android.launcher3.BubbleTextView; 45 import com.android.launcher3.FolderInfo; 46 import com.android.launcher3.ItemInfo; 47 import com.android.launcher3.Launcher; 48 import com.android.launcher3.LauncherAppWidgetInfo; 49 import com.android.launcher3.LauncherAppWidgetProviderInfo; 50 import com.android.launcher3.PromiseAppInfo; 51 import com.android.launcher3.R; 52 import com.android.launcher3.Utilities; 53 import com.android.launcher3.WorkspaceItemInfo; 54 import com.android.launcher3.compat.AppWidgetManagerCompat; 55 import com.android.launcher3.compat.PackageInstallerCompat; 56 import com.android.launcher3.folder.Folder; 57 import com.android.launcher3.folder.FolderIcon; 58 import com.android.launcher3.util.PackageManagerHelper; 59 import com.android.launcher3.views.FloatingIconView; 60 import com.android.launcher3.widget.PendingAppWidgetHostView; 61 import com.android.launcher3.widget.WidgetAddFlowHandler; 62 63 /** 64 * Class for handling clicks on workspace and all-apps items 65 */ 66 public class ItemClickHandler { 67 68 private static final String TAG = ItemClickHandler.class.getSimpleName(); 69 70 /** 71 * Instance used for click handling on items 72 */ 73 public static final OnClickListener INSTANCE = getInstance(null); 74 getInstance(String sourceContainer)75 public static final OnClickListener getInstance(String sourceContainer) { 76 return v -> onClick(v, sourceContainer); 77 } 78 onClick(View v, String sourceContainer)79 private static void onClick(View v, String sourceContainer) { 80 // Make sure that rogue clicks don't get through while allapps is launching, or after the 81 // view has detached (it's possible for this to happen if the view is removed mid touch). 82 if (v.getWindowToken() == null) return; 83 84 Launcher launcher = Launcher.getLauncher(v.getContext()); 85 if (!launcher.getWorkspace().isFinishedSwitchingState()) return; 86 87 Object tag = v.getTag(); 88 if (tag instanceof WorkspaceItemInfo) { 89 onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher, sourceContainer); 90 } else if (tag instanceof FolderInfo) { 91 if (v instanceof FolderIcon) { 92 onClickFolderIcon(v); 93 } 94 } else if (tag instanceof AppInfo) { 95 startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher, 96 sourceContainer == null ? CONTAINER_ALL_APPS: sourceContainer); 97 } else if (tag instanceof LauncherAppWidgetInfo) { 98 if (v instanceof PendingAppWidgetHostView) { 99 onClickPendingWidget((PendingAppWidgetHostView) v, launcher); 100 } 101 } 102 } 103 104 /** 105 * Event handler for a folder icon click. 106 * 107 * @param v The view that was clicked. Must be an instance of {@link FolderIcon}. 108 */ onClickFolderIcon(View v)109 private static void onClickFolderIcon(View v) { 110 Folder folder = ((FolderIcon) v).getFolder(); 111 if (!folder.isOpen() && !folder.isDestroyed()) { 112 // Open the requested folder 113 folder.animateOpen(); 114 } 115 } 116 117 /** 118 * Event handler for the app widget view which has not fully restored. 119 */ onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher)120 private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) { 121 if (launcher.getPackageManager().isSafeMode()) { 122 Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show(); 123 return; 124 } 125 126 final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); 127 if (v.isReadyForClickSetup()) { 128 LauncherAppWidgetProviderInfo appWidgetInfo = AppWidgetManagerCompat 129 .getInstance(launcher).findProvider(info.providerName, info.user); 130 if (appWidgetInfo == null) { 131 return; 132 } 133 WidgetAddFlowHandler addFlowHandler = new WidgetAddFlowHandler(appWidgetInfo); 134 135 if (info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { 136 if (!info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) { 137 // This should not happen, as we make sure that an Id is allocated during bind. 138 return; 139 } 140 addFlowHandler.startBindFlow(launcher, info.appWidgetId, info, 141 REQUEST_BIND_PENDING_APPWIDGET); 142 } else { 143 addFlowHandler.startConfigActivity(launcher, info, REQUEST_RECONFIGURE_APPWIDGET); 144 } 145 } else { 146 final String packageName = info.providerName.getPackageName(); 147 onClickPendingAppItem(v, launcher, packageName, info.installProgress >= 0); 148 } 149 } 150 onClickPendingAppItem(View v, Launcher launcher, String packageName, boolean downloadStarted)151 private static void onClickPendingAppItem(View v, Launcher launcher, String packageName, 152 boolean downloadStarted) { 153 if (downloadStarted) { 154 // If the download has started, simply direct to the market app. 155 startMarketIntentForPackage(v, launcher, packageName); 156 return; 157 } 158 UserHandle user = v.getTag() instanceof ItemInfo 159 ? ((ItemInfo) v.getTag()).user : Process.myUserHandle(); 160 new AlertDialog.Builder(launcher) 161 .setTitle(R.string.abandoned_promises_title) 162 .setMessage(R.string.abandoned_promise_explanation) 163 .setPositiveButton(R.string.abandoned_search, 164 (d, i) -> startMarketIntentForPackage(v, launcher, packageName)) 165 .setNeutralButton(R.string.abandoned_clean_this, 166 (d, i) -> launcher.getWorkspace() 167 .removeAbandonedPromise(packageName, user)) 168 .create().show(); 169 } 170 startMarketIntentForPackage(View v, Launcher launcher, String packageName)171 private static void startMarketIntentForPackage(View v, Launcher launcher, String packageName) { 172 ItemInfo item = (ItemInfo) v.getTag(); 173 if (Utilities.ATLEAST_Q) { 174 PackageInstallerCompat pkgInstaller = PackageInstallerCompat.getInstance(launcher); 175 SessionInfo sessionInfo = pkgInstaller.getActiveSessionInfo(item.user, packageName); 176 if (sessionInfo != null) { 177 LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class); 178 try { 179 launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null, 180 launcher.getActivityLaunchOptionsAsBundle(v)); 181 return; 182 } catch (Exception e) { 183 Log.e(TAG, "Unable to launch market intent for package=" + packageName, e); 184 } 185 } 186 } 187 188 // Fallback to using custom market intent. 189 Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName); 190 launcher.startActivitySafely(v, intent, item, null); 191 } 192 193 /** 194 * Event handler for an app shortcut click. 195 * 196 * @param v The view that was clicked. Must be a tagged with a {@link WorkspaceItemInfo}. 197 */ onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher, @Nullable String sourceContainer)198 public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher, 199 @Nullable String sourceContainer) { 200 if (shortcut.isDisabled()) { 201 final int disabledFlags = shortcut.runtimeStatusFlags 202 & WorkspaceItemInfo.FLAG_DISABLED_MASK; 203 if ((disabledFlags & 204 ~FLAG_DISABLED_SUSPENDED & 205 ~FLAG_DISABLED_QUIET_USER) == 0) { 206 // If the app is only disabled because of the above flags, launch activity anyway. 207 // Framework will tell the user why the app is suspended. 208 } else { 209 if (!TextUtils.isEmpty(shortcut.disabledMessage)) { 210 // Use a message specific to this shortcut, if it has one. 211 Toast.makeText(launcher, shortcut.disabledMessage, Toast.LENGTH_SHORT).show(); 212 return; 213 } 214 // Otherwise just use a generic error message. 215 int error = R.string.activity_not_available; 216 if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) { 217 error = R.string.safemode_shortcut_error; 218 } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0 || 219 (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) { 220 error = R.string.shortcut_not_available; 221 } 222 Toast.makeText(launcher, error, Toast.LENGTH_SHORT).show(); 223 return; 224 } 225 } 226 227 // Check for abandoned promise 228 if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) { 229 String packageName = shortcut.intent.getComponent() != null ? 230 shortcut.intent.getComponent().getPackageName() : shortcut.intent.getPackage(); 231 if (!TextUtils.isEmpty(packageName)) { 232 onClickPendingAppItem(v, launcher, packageName, 233 shortcut.hasStatusFlag(WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE)); 234 return; 235 } 236 } 237 238 // Start activities 239 startAppShortcutOrInfoActivity(v, shortcut, launcher, sourceContainer); 240 } 241 startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher, @Nullable String sourceContainer)242 private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher, 243 @Nullable String sourceContainer) { 244 Intent intent; 245 if (item instanceof PromiseAppInfo) { 246 PromiseAppInfo promiseAppInfo = (PromiseAppInfo) item; 247 intent = promiseAppInfo.getMarketIntent(launcher); 248 } else { 249 intent = item.getIntent(); 250 } 251 if (intent == null) { 252 throw new IllegalArgumentException("Input must have a valid intent"); 253 } 254 if (item instanceof WorkspaceItemInfo) { 255 WorkspaceItemInfo si = (WorkspaceItemInfo) item; 256 if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) 257 && Intent.ACTION_VIEW.equals(intent.getAction())) { 258 // make a copy of the intent that has the package set to null 259 // we do this because the platform sometimes disables instant 260 // apps temporarily (triggered by the user) and fallbacks to the 261 // web ui. This only works though if the package isn't set 262 intent = new Intent(intent); 263 intent.setPackage(null); 264 } 265 } 266 if (v != null && launcher.getAppTransitionManager().supportsAdaptiveIconAnimation()) { 267 // Preload the icon to reduce latency b/w swapping the floating view with the original. 268 FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */); 269 } 270 launcher.startActivitySafely(v, intent, item, sourceContainer); 271 } 272 } 273