• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
onClick(View v)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      */
onClickFolderIcon(View v)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      */
onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher)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 
onClickPendingAppItem(View v, Launcher launcher, String packageName, boolean downloadStarted)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 
startMarketIntentForPackage(View v, Launcher launcher, String packageName)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      */
onClickAppShortcut(View v, ShortcutInfo shortcut, Launcher launcher)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 
startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher)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