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.views;
17 
18 import static androidx.core.content.ContextCompat.getColorStateList;
19 
20 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
21 import static com.android.launcher3.LauncherState.EDIT_MODE;
22 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS;
25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS;
26 
27 import android.content.Context;
28 import android.content.Intent;
29 import android.graphics.Rect;
30 import android.graphics.RectF;
31 import android.graphics.drawable.Drawable;
32 import android.text.TextUtils;
33 import android.util.ArrayMap;
34 import android.util.AttributeSet;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.View.OnClickListener;
38 import android.view.View.OnLongClickListener;
39 import android.view.ViewGroup;
40 import android.widget.Toast;
41 
42 import androidx.annotation.Nullable;
43 import androidx.core.content.ContextCompat;
44 
45 import com.android.launcher3.AbstractFloatingView;
46 import com.android.launcher3.Launcher;
47 import com.android.launcher3.LauncherSettings;
48 import com.android.launcher3.R;
49 import com.android.launcher3.Utilities;
50 import com.android.launcher3.logging.StatsLogManager.EventEnum;
51 import com.android.launcher3.model.data.WorkspaceItemInfo;
52 import com.android.launcher3.popup.ArrowPopup;
53 import com.android.launcher3.shortcuts.DeepShortcutView;
54 import com.android.launcher3.testing.TestLogging;
55 import com.android.launcher3.testing.shared.TestProtocol;
56 import com.android.launcher3.widget.picker.WidgetsFullSheet;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 
61 /**
62  * Popup shown on long pressing an empty space in launcher
63  *
64  * @param <T> The context showing this popup.
65  */
66 public class OptionsPopupView<T extends Context & ActivityContext> extends ArrowPopup<T>
67         implements OnClickListener, OnLongClickListener {
68 
69     // An intent extra to indicate the horizontal scroll of the wallpaper.
70     private static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET";
71     private static final String EXTRA_WALLPAPER_FLAVOR = "com.android.launcher3.WALLPAPER_FLAVOR";
72     // An intent extra to indicate the launch source by launcher.
73     private static final String EXTRA_WALLPAPER_LAUNCH_SOURCE =
74             "com.android.wallpaper.LAUNCH_SOURCE";
75 
76     private final ArrayMap<View, OptionItem> mItemMap = new ArrayMap<>();
77     private RectF mTargetRect;
78     private boolean mShouldAddArrow;
79 
OptionsPopupView(Context context, AttributeSet attrs)80     public OptionsPopupView(Context context, AttributeSet attrs) {
81         this(context, attrs, 0);
82     }
83 
OptionsPopupView(Context context, AttributeSet attrs, int defStyleAttr)84     public OptionsPopupView(Context context, AttributeSet attrs, int defStyleAttr) {
85         super(context, attrs, defStyleAttr);
86     }
87 
setTargetRect(RectF targetRect)88     public void setTargetRect(RectF targetRect) {
89         mTargetRect = targetRect;
90     }
91 
92     @Override
onClick(View view)93     public void onClick(View view) {
94         handleViewClick(view);
95     }
96 
97     @Override
onLongClick(View view)98     public boolean onLongClick(View view) {
99         return handleViewClick(view);
100     }
101 
handleViewClick(View view)102     private boolean handleViewClick(View view) {
103         OptionItem item = mItemMap.get(view);
104         if (item == null) {
105             return false;
106         }
107         if (item.eventId.getId() > 0) {
108             mActivityContext.getStatsLogManager().logger().log(item.eventId);
109         }
110         if (item.clickListener.onLongClick(view)) {
111             close(true);
112             return true;
113         }
114         return false;
115     }
116 
117     @Override
onControllerInterceptTouchEvent(MotionEvent ev)118     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
119         if (ev.getAction() != MotionEvent.ACTION_DOWN) {
120             return false;
121         }
122         if (getPopupContainer().isEventOverView(this, ev)) {
123             return false;
124         }
125         close(true);
126         return true;
127     }
128 
129     @Override
isOfType(int type)130     protected boolean isOfType(int type) {
131         return (type & TYPE_OPTIONS_POPUP) != 0;
132     }
133 
setShouldAddArrow(boolean shouldAddArrow)134     public void setShouldAddArrow(boolean shouldAddArrow) {
135         mShouldAddArrow = shouldAddArrow;
136     }
137 
138     @Override
shouldAddArrow()139     protected boolean shouldAddArrow() {
140         return mShouldAddArrow;
141     }
142 
143     @Override
getTargetObjectLocation(Rect outPos)144     protected void getTargetObjectLocation(Rect outPos) {
145         mTargetRect.roundOut(outPos);
146     }
147 
148     @Override
assignMarginsAndBackgrounds(ViewGroup viewGroup)149     public void assignMarginsAndBackgrounds(ViewGroup viewGroup) {
150         assignMarginsAndBackgrounds(viewGroup,
151                 getColorStateList(getContext(), mColorIds[0]).getDefaultColor());
152         // last shortcut doesn't need bottom margin
153         final int count = viewGroup.getChildCount() - 1;
154         for (int i = 0; i < count; i++) {
155             // These are shortcuts and not shortcut containers, but they still need bottom margin
156             MarginLayoutParams mlp = (MarginLayoutParams) viewGroup.getChildAt(i).getLayoutParams();
157             mlp.bottomMargin = mChildContainerMargin;
158         }
159     }
160 
show( ActivityContext activityContext, RectF targetRect, List<OptionItem> items, boolean shouldAddArrow)161     public static <T extends Context & ActivityContext> OptionsPopupView<T> show(
162             ActivityContext activityContext,
163             RectF targetRect,
164             List<OptionItem> items,
165             boolean shouldAddArrow) {
166         return show(activityContext, targetRect, items, shouldAddArrow, 0 /* width */);
167     }
168 
169     @Nullable
show( @ullable ActivityContext activityContext, RectF targetRect, List<OptionItem> items, boolean shouldAddArrow, int width)170     private static <T extends Context & ActivityContext> OptionsPopupView<T> show(
171             @Nullable ActivityContext activityContext,
172             RectF targetRect,
173             List<OptionItem> items,
174             boolean shouldAddArrow,
175             int width) {
176         if (activityContext == null) {
177             return null;
178         }
179         OptionsPopupView<T> popup = (OptionsPopupView<T>) activityContext.getLayoutInflater()
180                 .inflate(R.layout.longpress_options_menu, activityContext.getDragLayer(), false);
181         popup.mTargetRect = targetRect;
182         popup.setShouldAddArrow(shouldAddArrow);
183 
184         for (OptionItem item : items) {
185             DeepShortcutView view = popup.inflateAndAdd(R.layout.system_shortcut, popup);
186             if (width > 0) {
187                 view.getLayoutParams().width = width;
188             }
189             view.getIconView().setBackgroundDrawable(item.icon);
190             view.getBubbleText().setText(item.label);
191             view.setOnClickListener(popup);
192             view.setOnLongClickListener(popup);
193             popup.mItemMap.put(view, item);
194         }
195 
196         popup.show();
197         return popup;
198     }
199 
200     /**
201      * Returns the list of supported actions
202      */
getOptions(Launcher launcher)203     public static ArrayList<OptionItem> getOptions(Launcher launcher) {
204         ArrayList<OptionItem> options = new ArrayList<>();
205         options.add(new OptionItem(launcher,
206                 R.string.styles_wallpaper_button_text,
207                 R.drawable.ic_palette,
208                 IGNORE,
209                 OptionsPopupView::startWallpaperPicker));
210         if (WIDGETS_ENABLED) {
211             options.add(new OptionItem(launcher,
212                     R.string.widget_button_text,
213                     R.drawable.ic_widget,
214                     LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS,
215                     OptionsPopupView::onWidgetsClicked));
216         }
217         if (MULTI_SELECT_EDIT_MODE.get()) {
218             options.add(new OptionItem(launcher,
219                     R.string.edit_home_screen,
220                     R.drawable.enter_home_gardening_icon,
221                     LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
222                     OptionsPopupView::enterHomeGardening));
223         }
224         options.add(new OptionItem(launcher,
225                 R.string.settings_button_text,
226                 R.drawable.ic_setting,
227                 LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
228                 OptionsPopupView::startSettings));
229         return options;
230     }
231 
enterHomeGardening(View view)232     private static boolean enterHomeGardening(View view) {
233         Launcher launcher = Launcher.getLauncher(view.getContext());
234         launcher.getStateManager().goToState(EDIT_MODE);
235         return true;
236     }
237 
onWidgetsClicked(View view)238     private static boolean onWidgetsClicked(View view) {
239         return openWidgets(Launcher.getLauncher(view.getContext())) != null;
240     }
241 
242     /** Returns WidgetsFullSheet that was opened, or null if nothing was opened. */
243     @Nullable
openWidgets(Launcher launcher)244     public static WidgetsFullSheet openWidgets(Launcher launcher) {
245         if (launcher.getPackageManager().isSafeMode()) {
246             Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
247             return null;
248         } else {
249             AbstractFloatingView floatingView = AbstractFloatingView.getTopOpenViewWithType(
250                     launcher, TYPE_WIDGETS_FULL_SHEET);
251             if (floatingView != null) {
252                 return (WidgetsFullSheet) floatingView;
253             }
254             return WidgetsFullSheet.show(launcher, true /* animated */);
255         }
256     }
257 
startSettings(View view)258     private static boolean startSettings(View view) {
259         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startSettings");
260         Launcher launcher = Launcher.getLauncher(view.getContext());
261         launcher.startActivity(new Intent(Intent.ACTION_APPLICATION_PREFERENCES)
262                 .setPackage(launcher.getPackageName())
263                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK));
264         return true;
265     }
266 
267     /**
268      * Event handler for the wallpaper picker button that appears after a long press
269      * on the home screen.
270      */
startWallpaperPicker(View v)271     private static boolean startWallpaperPicker(View v) {
272         Launcher launcher = Launcher.getLauncher(v.getContext());
273         if (!Utilities.isWallpaperAllowed(launcher)) {
274             String message = launcher.getStringCache() != null
275                     ? launcher.getStringCache().disabledByAdminMessage
276                     : launcher.getString(R.string.msg_disabled_by_admin);
277             Toast.makeText(launcher, message, Toast.LENGTH_SHORT).show();
278             return false;
279         }
280         Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER)
281                 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
282                 .putExtra(EXTRA_WALLPAPER_OFFSET,
283                         launcher.getWorkspace().getWallpaperOffsetForCenterPage())
284                 .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher")
285                 .putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper");
286         String pickerPackage = launcher.getString(R.string.wallpaper_picker_package);
287         if (!TextUtils.isEmpty(pickerPackage)) {
288             intent.setPackage(pickerPackage);
289         }
290         return launcher.startActivitySafely(v, intent, placeholderInfo(intent)) != null;
291     }
292 
placeholderInfo(Intent intent)293     static WorkspaceItemInfo placeholderInfo(Intent intent) {
294         WorkspaceItemInfo placeholderInfo = new WorkspaceItemInfo();
295         placeholderInfo.intent = intent;
296         placeholderInfo.container = LauncherSettings.Favorites.CONTAINER_SETTINGS;
297         return placeholderInfo;
298     }
299 
300     public static class OptionItem {
301 
302         // Used to create AccessibilityNodeInfo in AccessibilityActionsView.java.
303         public final int labelRes;
304 
305         public final CharSequence label;
306         public final Drawable icon;
307         public final EventEnum eventId;
308         public final OnLongClickListener clickListener;
309 
OptionItem(Context context, int labelRes, int iconRes, EventEnum eventId, OnLongClickListener clickListener)310         public OptionItem(Context context, int labelRes, int iconRes, EventEnum eventId,
311                 OnLongClickListener clickListener) {
312             this.labelRes = labelRes;
313             this.label = context.getText(labelRes);
314             this.icon = ContextCompat.getDrawable(context, iconRes);
315             this.eventId = eventId;
316             this.clickListener = clickListener;
317         }
318 
OptionItem(CharSequence label, Drawable icon, EventEnum eventId, OnLongClickListener clickListener)319         public OptionItem(CharSequence label, Drawable icon, EventEnum eventId,
320                 OnLongClickListener clickListener) {
321             this.labelRes = 0;
322             this.label = label;
323             this.icon = icon;
324             this.eventId = eventId;
325             this.clickListener = clickListener;
326         }
327     }
328 }
329