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