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