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