1 /* 2 * Copyright (C) 2017 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.widget; 17 18 import android.content.Context; 19 import android.graphics.Point; 20 import android.graphics.Rect; 21 import android.util.AttributeSet; 22 import android.util.Log; 23 import android.view.View; 24 import android.view.View.OnClickListener; 25 import android.view.View.OnLongClickListener; 26 import android.widget.Toast; 27 28 import androidx.annotation.GuardedBy; 29 import androidx.annotation.Nullable; 30 import androidx.core.view.ViewCompat; 31 32 import com.android.launcher3.DeviceProfile; 33 import com.android.launcher3.DragSource; 34 import com.android.launcher3.DropTarget.DragObject; 35 import com.android.launcher3.Insettable; 36 import com.android.launcher3.Launcher; 37 import com.android.launcher3.R; 38 import com.android.launcher3.Utilities; 39 import com.android.launcher3.dragndrop.DragOptions; 40 import com.android.launcher3.popup.PopupDataProvider; 41 import com.android.launcher3.testing.TestLogging; 42 import com.android.launcher3.testing.TestProtocol; 43 import com.android.launcher3.touch.ItemLongClickListener; 44 import com.android.launcher3.util.SystemUiController; 45 import com.android.launcher3.util.Themes; 46 import com.android.launcher3.views.AbstractSlideInView; 47 import com.android.launcher3.views.ArrowTipView; 48 49 /** 50 * Base class for various widgets popup 51 */ 52 public abstract class BaseWidgetSheet extends AbstractSlideInView<Launcher> 53 implements OnClickListener, OnLongClickListener, DragSource, 54 PopupDataProvider.PopupDataChangeListener, Insettable { 55 /** The default number of cells that can fit horizontally in a widget sheet. */ 56 protected static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4; 57 /** 58 * The maximum scale, [0, 1], of the device screen width that the widgets picker can consume 59 * on large screen devices. 60 */ 61 protected static final float MAX_WIDTH_SCALE_FOR_LARGER_SCREEN = 0.89f; 62 63 protected static final String KEY_WIDGETS_EDUCATION_TIP_SEEN = 64 "launcher.widgets_education_tip_seen"; 65 protected final Rect mInsets = new Rect(); 66 67 /* Touch handling related member variables. */ 68 private Toast mWidgetInstructionToast; 69 70 private int mContentHorizontalMarginInPx; 71 BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr)72 public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) { 73 super(context, attrs, defStyleAttr); 74 mContentHorizontalMarginInPx = getResources().getDimensionPixelSize( 75 R.dimen.widget_list_horizontal_margin); 76 } 77 getScrimColor(Context context)78 protected int getScrimColor(Context context) { 79 return context.getResources().getColor(R.color.widgets_picker_scrim); 80 } 81 82 @Override onAttachedToWindow()83 protected void onAttachedToWindow() { 84 super.onAttachedToWindow(); 85 mActivityContext.getPopupDataProvider().setChangeListener(this); 86 } 87 88 @Override onDetachedFromWindow()89 protected void onDetachedFromWindow() { 90 super.onDetachedFromWindow(); 91 mActivityContext.getPopupDataProvider().setChangeListener(null); 92 } 93 94 @Override onClick(View v)95 public final void onClick(View v) { 96 Object tag = null; 97 if (v instanceof WidgetCell) { 98 tag = v.getTag(); 99 } else if (v.getParent() instanceof WidgetCell) { 100 tag = ((WidgetCell) v.getParent()).getTag(); 101 } 102 if (tag instanceof PendingAddShortcutInfo) { 103 mWidgetInstructionToast = showShortcutToast(getContext(), mWidgetInstructionToast); 104 } else { 105 mWidgetInstructionToast = showWidgetToast(getContext(), mWidgetInstructionToast); 106 } 107 108 } 109 110 @Override onLongClick(View v)111 public boolean onLongClick(View v) { 112 if (TestProtocol.sDebugTracing) { 113 Log.d(TestProtocol.NO_DROP_TARGET, "1"); 114 } 115 TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick"); 116 v.cancelLongPress(); 117 if (!ItemLongClickListener.canStartDrag(mActivityContext)) return false; 118 119 if (v instanceof WidgetCell) { 120 return beginDraggingWidget((WidgetCell) v); 121 } else if (v.getParent() instanceof WidgetCell) { 122 return beginDraggingWidget((WidgetCell) v.getParent()); 123 } 124 return true; 125 } 126 127 @Override setInsets(Rect insets)128 public void setInsets(Rect insets) { 129 mInsets.set(insets); 130 int contentHorizontalMarginInPx = getResources().getDimensionPixelSize( 131 R.dimen.widget_list_horizontal_margin); 132 if (contentHorizontalMarginInPx != mContentHorizontalMarginInPx) { 133 onContentHorizontalMarginChanged(contentHorizontalMarginInPx); 134 mContentHorizontalMarginInPx = contentHorizontalMarginInPx; 135 } 136 } 137 138 /** Called when the horizontal margin of the content view has changed. */ onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)139 protected abstract void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx); 140 141 /** 142 * Measures the dimension of this view and its children by taking system insets, navigation bar, 143 * status bar, into account. 144 */ 145 @GuardedBy("MainThread") doMeasure(int widthMeasureSpec, int heightMeasureSpec)146 protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { 147 DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); 148 int widthUsed; 149 if (mInsets.bottom > 0) { 150 widthUsed = mInsets.left + mInsets.right; 151 } else { 152 Rect padding = deviceProfile.workspacePadding; 153 widthUsed = Math.max(padding.left + padding.right, 154 2 * (mInsets.left + mInsets.right)); 155 } 156 157 if (deviceProfile.isTablet || deviceProfile.isTwoPanels) { 158 // In large screen devices, we restrict the width of the widgets picker to show part of 159 // the home screen. Let's ensure the minimum width used is at least the minimum width 160 // that isn't taken by the widgets picker. 161 int minUsedWidth = (int) (deviceProfile.availableWidthPx 162 * (1 - MAX_WIDTH_SCALE_FOR_LARGER_SCREEN)); 163 widthUsed = Math.max(widthUsed, minUsedWidth); 164 } 165 166 int heightUsed = mInsets.top + deviceProfile.edgeMarginPx; 167 measureChildWithMargins(mContent, widthMeasureSpec, 168 widthUsed, heightMeasureSpec, heightUsed); 169 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 170 MeasureSpec.getSize(heightMeasureSpec)); 171 } 172 173 /** Returns the number of cells that can fit horizontally in a given {@code content}. */ computeMaxHorizontalSpans(View content, int contentHorizontalPaddingPx)174 protected int computeMaxHorizontalSpans(View content, int contentHorizontalPaddingPx) { 175 DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); 176 int availableWidth = content.getMeasuredWidth() - contentHorizontalPaddingPx; 177 Point cellSize = deviceProfile.getCellSize(); 178 if (cellSize.x > 0) { 179 return availableWidth / cellSize.x; 180 } 181 return DEFAULT_MAX_HORIZONTAL_SPANS; 182 } 183 beginDraggingWidget(WidgetCell v)184 private boolean beginDraggingWidget(WidgetCell v) { 185 if (TestProtocol.sDebugTracing) { 186 Log.d(TestProtocol.NO_DROP_TARGET, "2"); 187 } 188 // Get the widget preview as the drag representation 189 WidgetImageView image = v.getWidgetView(); 190 191 // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and 192 // we abort the drag. 193 if (image.getDrawable() == null && v.getAppWidgetHostViewPreview() == null) { 194 return false; 195 } 196 197 PendingItemDragHelper dragHelper = new PendingItemDragHelper(v); 198 // RemoteViews are being rendered in AppWidgetHostView in WidgetCell. And thus, the scale of 199 // RemoteViews is equivalent to the AppWidgetHostView scale. 200 dragHelper.setRemoteViewsPreview(v.getRemoteViewsPreview(), v.getAppWidgetHostViewScale()); 201 dragHelper.setAppWidgetHostViewPreview(v.getAppWidgetHostViewPreview()); 202 203 if (image.getDrawable() != null) { 204 int[] loc = new int[2]; 205 getPopupContainer().getLocationInDragLayer(image, loc); 206 207 dragHelper.startDrag(image.getBitmapBounds(), image.getDrawable().getIntrinsicWidth(), 208 image.getWidth(), new Point(loc[0], loc[1]), this, new DragOptions()); 209 } else { 210 NavigableAppWidgetHostView preview = v.getAppWidgetHostViewPreview(); 211 int[] loc = new int[2]; 212 getPopupContainer().getLocationInDragLayer(preview, loc); 213 Rect r = new Rect(); 214 preview.getWorkspaceVisualDragBounds(r); 215 dragHelper.startDrag(r, preview.getMeasuredWidth(), preview.getMeasuredWidth(), 216 new Point(loc[0], loc[1]), this, new DragOptions()); 217 } 218 close(true); 219 return true; 220 } 221 222 // 223 // Drag related handling methods that implement {@link DragSource} interface. 224 // 225 226 @Override onDropCompleted(View target, DragObject d, boolean success)227 public void onDropCompleted(View target, DragObject d, boolean success) { } 228 229 onCloseComplete()230 protected void onCloseComplete() { 231 super.onCloseComplete(); 232 clearNavBarColor(); 233 } 234 clearNavBarColor()235 protected void clearNavBarColor() { 236 getSystemUiController().updateUiState( 237 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0); 238 } 239 setupNavBarColor()240 protected void setupNavBarColor() { 241 boolean isSheetDark = Themes.getAttrBoolean(getContext(), R.attr.isMainColorDark); 242 getSystemUiController().updateUiState( 243 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 244 isSheetDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV); 245 } 246 getSystemUiController()247 protected SystemUiController getSystemUiController() { 248 return mActivityContext.getSystemUiController(); 249 } 250 251 /** 252 * Show Widget tap toast prompting user to drag instead 253 */ showWidgetToast(Context context, Toast toast)254 public static Toast showWidgetToast(Context context, Toast toast) { 255 // Let the user know that they have to long press to add a widget 256 if (toast != null) { 257 toast.cancel(); 258 } 259 260 CharSequence msg = Utilities.wrapForTts( 261 context.getText(R.string.long_press_widget_to_add), 262 context.getString(R.string.long_accessible_way_to_add)); 263 toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); 264 toast.show(); 265 return toast; 266 } 267 268 /** 269 * Show shortcut tap toast prompting user to drag instead. 270 */ showShortcutToast(Context context, Toast toast)271 private static Toast showShortcutToast(Context context, Toast toast) { 272 // Let the user know that they have to long press to add a widget 273 if (toast != null) { 274 toast.cancel(); 275 } 276 277 CharSequence msg = Utilities.wrapForTts( 278 context.getText(R.string.long_press_shortcut_to_add), 279 context.getString(R.string.long_accessible_way_to_add_shortcut)); 280 toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); 281 toast.show(); 282 return toast; 283 } 284 285 /** Shows education tip on top center of {@code view} if view is laid out. */ 286 @Nullable showEducationTipOnViewIfPossible(@ullable View view)287 protected ArrowTipView showEducationTipOnViewIfPossible(@Nullable View view) { 288 if (view == null || !ViewCompat.isLaidOut(view)) { 289 return null; 290 } 291 int[] coords = new int[2]; 292 view.getLocationOnScreen(coords); 293 ArrowTipView arrowTipView = 294 new ArrowTipView(mActivityContext, /* isPointingUp= */ false).showAtLocation( 295 getContext().getString(R.string.long_press_widget_to_add), 296 /* arrowXCoord= */coords[0] + view.getWidth() / 2, 297 /* yCoord= */coords[1]); 298 if (arrowTipView != null) { 299 mActivityContext.getSharedPrefs().edit() 300 .putBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, true).apply(); 301 } 302 return arrowTipView; 303 } 304 305 /** Returns {@code true} if tip has previously been shown on any of {@link BaseWidgetSheet}. */ hasSeenEducationTip()306 protected boolean hasSeenEducationTip() { 307 return mActivityContext.getSharedPrefs().getBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, false) 308 || Utilities.IS_RUNNING_IN_TEST_HARNESS; 309 } 310 } 311