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_APP_LAUNCH_TAP; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN; 22 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER; 23 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER; 24 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER; 25 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE; 26 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED; 27 28 import android.app.AlertDialog; 29 import android.app.PendingIntent; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentSender; 33 import android.content.pm.LauncherApps; 34 import android.content.pm.PackageInstaller.SessionInfo; 35 import android.os.Process; 36 import android.os.UserHandle; 37 import android.text.TextUtils; 38 import android.util.Log; 39 import android.view.View; 40 import android.view.View.OnClickListener; 41 import android.widget.Toast; 42 43 import com.android.launcher3.BubbleTextView; 44 import com.android.launcher3.Launcher; 45 import com.android.launcher3.R; 46 import com.android.launcher3.Utilities; 47 import com.android.launcher3.folder.Folder; 48 import com.android.launcher3.folder.FolderIcon; 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.SearchActionItemInfo; 56 import com.android.launcher3.model.data.WorkspaceItemInfo; 57 import com.android.launcher3.pm.InstallSessionHelper; 58 import com.android.launcher3.testing.TestLogging; 59 import com.android.launcher3.testing.TestProtocol; 60 import com.android.launcher3.util.PackageManagerHelper; 61 import com.android.launcher3.views.FloatingIconView; 62 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 63 import com.android.launcher3.widget.PendingAppWidgetHostView; 64 import com.android.launcher3.widget.WidgetAddFlowHandler; 65 import com.android.launcher3.widget.WidgetManagerHelper; 66 67 /** 68 * Class for handling clicks on workspace and all-apps items 69 */ 70 public class ItemClickHandler { 71 72 private static final String TAG = ItemClickHandler.class.getSimpleName(); 73 74 /** 75 * Instance used for click handling on items 76 */ 77 public static final OnClickListener INSTANCE = ItemClickHandler::onClick; 78 onClick(View v)79 private static void onClick(View v) { 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); 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 ); 97 } else if (tag instanceof LauncherAppWidgetInfo) { 98 if (v instanceof PendingAppWidgetHostView) { 99 onClickPendingWidget((PendingAppWidgetHostView) v, launcher); 100 } 101 } else if (tag instanceof SearchActionItemInfo) { 102 onClickSearchAction(launcher, (SearchActionItemInfo) tag); 103 } 104 } 105 106 /** 107 * Event handler for a folder icon click. 108 * 109 * @param v The view that was clicked. Must be an instance of {@link FolderIcon}. 110 */ onClickFolderIcon(View v)111 private static void onClickFolderIcon(View v) { 112 Folder folder = ((FolderIcon) v).getFolder(); 113 if (!folder.isOpen() && !folder.isDestroyed()) { 114 // Open the requested folder 115 folder.animateOpen(); 116 StatsLogManager.newInstance(v.getContext()).logger().withItemInfo(folder.mInfo) 117 .log(LAUNCHER_FOLDER_OPEN); 118 } 119 } 120 121 /** 122 * Event handler for the app widget view which has not fully restored. 123 */ onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher)124 private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) { 125 if (launcher.getPackageManager().isSafeMode()) { 126 Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show(); 127 return; 128 } 129 130 final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); 131 if (v.isReadyForClickSetup()) { 132 LauncherAppWidgetProviderInfo appWidgetInfo = new WidgetManagerHelper(launcher) 133 .findProvider(info.providerName, info.user); 134 if (appWidgetInfo == null) { 135 return; 136 } 137 WidgetAddFlowHandler addFlowHandler = new WidgetAddFlowHandler(appWidgetInfo); 138 139 if (info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { 140 if (!info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) { 141 // This should not happen, as we make sure that an Id is allocated during bind. 142 return; 143 } 144 addFlowHandler.startBindFlow(launcher, info.appWidgetId, info, 145 REQUEST_BIND_PENDING_APPWIDGET); 146 } else { 147 addFlowHandler.startConfigActivity(launcher, info, REQUEST_RECONFIGURE_APPWIDGET); 148 } 149 } else { 150 final String packageName = info.providerName.getPackageName(); 151 onClickPendingAppItem(v, launcher, packageName, info.installProgress >= 0); 152 } 153 } 154 onClickPendingAppItem(View v, Launcher launcher, String packageName, boolean downloadStarted)155 private static void onClickPendingAppItem(View v, Launcher launcher, String packageName, 156 boolean downloadStarted) { 157 if (downloadStarted) { 158 // If the download has started, simply direct to the market app. 159 startMarketIntentForPackage(v, launcher, packageName); 160 return; 161 } 162 UserHandle user = v.getTag() instanceof ItemInfo 163 ? ((ItemInfo) v.getTag()).user : Process.myUserHandle(); 164 new AlertDialog.Builder(launcher) 165 .setTitle(R.string.abandoned_promises_title) 166 .setMessage(R.string.abandoned_promise_explanation) 167 .setPositiveButton(R.string.abandoned_search, 168 (d, i) -> startMarketIntentForPackage(v, launcher, packageName)) 169 .setNeutralButton(R.string.abandoned_clean_this, 170 (d, i) -> launcher.getWorkspace() 171 .removeAbandonedPromise(packageName, user)) 172 .create().show(); 173 } 174 startMarketIntentForPackage(View v, Launcher launcher, String packageName)175 private static void startMarketIntentForPackage(View v, Launcher launcher, String packageName) { 176 ItemInfo item = (ItemInfo) v.getTag(); 177 if (Utilities.ATLEAST_Q) { 178 SessionInfo sessionInfo = InstallSessionHelper.INSTANCE.get(launcher) 179 .getActiveSessionInfo(item.user, packageName); 180 if (sessionInfo != null) { 181 LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class); 182 try { 183 launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null, 184 launcher.getActivityLaunchOptions(v, item).toBundle()); 185 return; 186 } catch (Exception e) { 187 Log.e(TAG, "Unable to launch market intent for package=" + packageName, e); 188 } 189 } 190 } 191 192 // Fallback to using custom market intent. 193 Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName); 194 launcher.startActivitySafely(v, intent, item); 195 } 196 197 /** 198 * Handles clicking on a disabled shortcut 199 * 200 * @return true iff the disabled item click has been handled. 201 */ handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context)202 public static boolean handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context) { 203 final int disabledFlags = shortcut.runtimeStatusFlags 204 & WorkspaceItemInfo.FLAG_DISABLED_MASK; 205 if ((disabledFlags 206 & ~FLAG_DISABLED_SUSPENDED 207 & ~FLAG_DISABLED_QUIET_USER) == 0) { 208 // If the app is only disabled because of the above flags, launch activity anyway. 209 // Framework will tell the user why the app is suspended. 210 return false; 211 } else { 212 if (!TextUtils.isEmpty(shortcut.disabledMessage)) { 213 // Use a message specific to this shortcut, if it has one. 214 Toast.makeText(context, shortcut.disabledMessage, Toast.LENGTH_SHORT).show(); 215 return true; 216 } 217 // Otherwise just use a generic error message. 218 int error = R.string.activity_not_available; 219 if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) { 220 error = R.string.safemode_shortcut_error; 221 } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0 222 || (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) { 223 error = R.string.shortcut_not_available; 224 } 225 Toast.makeText(context, error, Toast.LENGTH_SHORT).show(); 226 return true; 227 } 228 } 229 230 /** 231 * Event handler for an app shortcut click. 232 * 233 * @param v The view that was clicked. Must be a tagged with a {@link WorkspaceItemInfo}. 234 */ onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher)235 public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) { 236 if (shortcut.isDisabled() && handleDisabledItemClicked(shortcut, launcher)) { 237 return; 238 } 239 240 // Check for abandoned promise 241 if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) { 242 String packageName = shortcut.getIntent().getComponent() != null 243 ? shortcut.getIntent().getComponent().getPackageName() 244 : shortcut.getIntent().getPackage(); 245 if (!TextUtils.isEmpty(packageName)) { 246 onClickPendingAppItem( 247 v, 248 launcher, 249 packageName, 250 (shortcut.runtimeStatusFlags 251 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0); 252 return; 253 } 254 } 255 256 // Start activities 257 startAppShortcutOrInfoActivity(v, shortcut, launcher); 258 } 259 260 /** 261 * Event handler for a {@link SearchActionItemInfo} click 262 */ onClickSearchAction(Launcher launcher, SearchActionItemInfo itemInfo)263 public static void onClickSearchAction(Launcher launcher, SearchActionItemInfo itemInfo) { 264 if (itemInfo.getIntent() != null) { 265 if (itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START_FOR_RESULT)) { 266 launcher.startActivityForResult(itemInfo.getIntent(), 0); 267 } else { 268 launcher.startActivity(itemInfo.getIntent()); 269 } 270 } else if (itemInfo.getPendingIntent() != null) { 271 try { 272 PendingIntent pendingIntent = itemInfo.getPendingIntent(); 273 if (!itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START)) { 274 pendingIntent.send(); 275 } else if (itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START_FOR_RESULT)) { 276 launcher.startIntentSenderForResult(pendingIntent.getIntentSender(), 0, null, 0, 277 0, 0); 278 } else { 279 launcher.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); 280 } 281 } catch (PendingIntent.CanceledException | IntentSender.SendIntentException e) { 282 Toast.makeText(launcher, 283 launcher.getResources().getText(R.string.shortcut_not_available), 284 Toast.LENGTH_SHORT).show(); 285 } 286 } 287 launcher.getStatsLogManager().logger().withItemInfo(itemInfo).log(LAUNCHER_APP_LAUNCH_TAP); 288 } 289 startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher)290 private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) { 291 TestLogging.recordEvent( 292 TestProtocol.SEQUENCE_MAIN, "start: startAppShortcutOrInfoActivity"); 293 Intent intent; 294 if (item instanceof ItemInfoWithIcon 295 && (((ItemInfoWithIcon) item).runtimeStatusFlags 296 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) { 297 ItemInfoWithIcon appInfo = (ItemInfoWithIcon) item; 298 intent = new PackageManagerHelper(launcher) 299 .getMarketIntent(appInfo.getTargetComponent().getPackageName()); 300 } else { 301 intent = item.getIntent(); 302 } 303 if (intent == null) { 304 throw new IllegalArgumentException("Input must have a valid intent"); 305 } 306 if (item instanceof WorkspaceItemInfo) { 307 WorkspaceItemInfo si = (WorkspaceItemInfo) item; 308 if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) 309 && Intent.ACTION_VIEW.equals(intent.getAction())) { 310 // make a copy of the intent that has the package set to null 311 // we do this because the platform sometimes disables instant 312 // apps temporarily (triggered by the user) and fallbacks to the 313 // web ui. This only works though if the package isn't set 314 intent = new Intent(intent); 315 intent.setPackage(null); 316 } 317 } 318 if (v != null && launcher.supportsAdaptiveIconAnimation(v)) { 319 // Preload the icon to reduce latency b/w swapping the floating view with the original. 320 FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */); 321 } 322 launcher.startActivitySafely(v, intent, item); 323 } 324 } 325