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