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