1 package com.android.launcher3;
2 
3 import static android.appwidget.AppWidgetHostView.getDefaultPaddingForWidget;
4 
5 import static com.android.launcher3.CellLayout.SPRING_LOADED_PROGRESS;
6 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT;
7 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH;
8 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_COMPLETED;
9 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_STARTED;
10 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X;
11 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y;
12 
13 import android.animation.Animator;
14 import android.animation.AnimatorListenerAdapter;
15 import android.animation.AnimatorSet;
16 import android.animation.ObjectAnimator;
17 import android.animation.PropertyValuesHolder;
18 import android.appwidget.AppWidgetProviderInfo;
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.graphics.drawable.Drawable;
22 import android.graphics.drawable.GradientDrawable;
23 import android.util.AttributeSet;
24 import android.view.KeyEvent;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.widget.ImageButton;
28 import android.widget.ImageView;
29 
30 import androidx.annotation.Nullable;
31 import androidx.annotation.Px;
32 
33 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
34 import com.android.launcher3.dragndrop.DragLayer;
35 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
36 import com.android.launcher3.logging.InstanceId;
37 import com.android.launcher3.logging.InstanceIdSequence;
38 import com.android.launcher3.model.data.ItemInfo;
39 import com.android.launcher3.util.PendingRequestArgs;
40 import com.android.launcher3.views.ArrowTipView;
41 import com.android.launcher3.widget.LauncherAppWidgetHostView;
42 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
43 import com.android.launcher3.widget.util.WidgetSizes;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 
48 public class AppWidgetResizeFrame extends AbstractFloatingView implements View.OnKeyListener {
49     private static final int SNAP_DURATION = 150;
50     private static final float DIMMED_HANDLE_ALPHA = 0f;
51     private static final float RESIZE_THRESHOLD = 0.66f;
52 
53     private static final String KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
54             "launcher.reconfigurable_widget_education_tip_seen";
55     private static final Rect sTmpRect = new Rect();
56     private static final Rect sTmpRect2 = new Rect();
57 
58     private static final int HANDLE_COUNT = 4;
59     private static final int INDEX_LEFT = 0;
60     private static final int INDEX_TOP = 1;
61     private static final int INDEX_RIGHT = 2;
62     private static final int INDEX_BOTTOM = 3;
63     private static final float MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE = 0.5f;
64 
65     private final Launcher mLauncher;
66     private final DragViewStateAnnouncer mStateAnnouncer;
67     private final FirstFrameAnimatorHelper mFirstFrameAnimatorHelper;
68 
69     private final View[] mDragHandles = new View[HANDLE_COUNT];
70     private final List<Rect> mSystemGestureExclusionRects = new ArrayList<>(HANDLE_COUNT);
71     private final OnAttachStateChangeListener mWidgetViewAttachStateChangeListener =
72             new OnAttachStateChangeListener() {
73                 @Override
74                 public void onViewAttachedToWindow(View view) {
75                     // Do nothing
76                 }
77 
78                 @Override
79                 public void onViewDetachedFromWindow(View view) {
80                     // When the app widget view is detached, we should close the resize frame.
81                     // An example is when the dragging starts, the widget view is detached from
82                     // CellLayout and then reattached to DragLayout.
83                     close(false);
84                 }
85             };
86 
87 
88     private LauncherAppWidgetHostView mWidgetView;
89     private CellLayout mCellLayout;
90     private DragLayer mDragLayer;
91     private ImageButton mReconfigureButton;
92 
93     private Rect mWidgetPadding;
94 
95     private final int mBackgroundPadding;
96     private final int mTouchTargetWidth;
97 
98     private final int[] mDirectionVector = new int[2];
99     private final int[] mLastDirectionVector = new int[2];
100 
101     private final IntRange mTempRange1 = new IntRange();
102     private final IntRange mTempRange2 = new IntRange();
103 
104     private final IntRange mDeltaXRange = new IntRange();
105     private final IntRange mBaselineX = new IntRange();
106 
107     private final IntRange mDeltaYRange = new IntRange();
108     private final IntRange mBaselineY = new IntRange();
109 
110     private final InstanceId logInstanceId = new InstanceIdSequence().newInstanceId();
111 
112     private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper;
113 
114     /**
115      * In the two panel UI, it is not possible to resize a widget to cross its host
116      * {@link CellLayout}'s sibling. When this happens, we gradually reduce the opacity of the
117      * sibling {@link CellLayout} from 1f to
118      * {@link #MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE}.
119      */
120     private final float mDragAcrossTwoPanelOpacityMargin;
121 
122     private boolean mLeftBorderActive;
123     private boolean mRightBorderActive;
124     private boolean mTopBorderActive;
125     private boolean mBottomBorderActive;
126 
127     private boolean mHorizontalResizeActive;
128     private boolean mVerticalResizeActive;
129 
130     private int mRunningHInc;
131     private int mRunningVInc;
132     private int mMinHSpan;
133     private int mMinVSpan;
134     private int mMaxHSpan;
135     private int mMaxVSpan;
136     private int mDeltaX;
137     private int mDeltaY;
138     private int mDeltaXAddOn;
139     private int mDeltaYAddOn;
140 
141     private int mTopTouchRegionAdjustment = 0;
142     private int mBottomTouchRegionAdjustment = 0;
143 
144     private int mXDown, mYDown;
145 
AppWidgetResizeFrame(Context context)146     public AppWidgetResizeFrame(Context context) {
147         this(context, null);
148     }
149 
AppWidgetResizeFrame(Context context, AttributeSet attrs)150     public AppWidgetResizeFrame(Context context, AttributeSet attrs) {
151         this(context, attrs, 0);
152     }
153 
AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr)154     public AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr) {
155         super(context, attrs, defStyleAttr);
156 
157         mLauncher = Launcher.getLauncher(context);
158         mStateAnnouncer = DragViewStateAnnouncer.createFor(this);
159 
160         mBackgroundPadding = getResources()
161                 .getDimensionPixelSize(R.dimen.resize_frame_background_padding);
162         mTouchTargetWidth = 2 * mBackgroundPadding;
163         mFirstFrameAnimatorHelper = new FirstFrameAnimatorHelper(this);
164 
165         for (int i = 0; i < HANDLE_COUNT; i++) {
166             mSystemGestureExclusionRects.add(new Rect());
167         }
168 
169         mDragAcrossTwoPanelOpacityMargin = mLauncher.getResources().getDimensionPixelSize(
170                 R.dimen.resize_frame_invalid_drag_across_two_panel_opacity_margin);
171         mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer());
172     }
173 
174     @Override
onFinishInflate()175     protected void onFinishInflate() {
176         super.onFinishInflate();
177 
178         mDragHandles[INDEX_LEFT] = findViewById(R.id.widget_resize_left_handle);
179         mDragHandles[INDEX_TOP] = findViewById(R.id.widget_resize_top_handle);
180         mDragHandles[INDEX_RIGHT] = findViewById(R.id.widget_resize_right_handle);
181         mDragHandles[INDEX_BOTTOM] = findViewById(R.id.widget_resize_bottom_handle);
182     }
183 
184     @Override
onLayout(boolean changed, int l, int t, int r, int b)185     protected void onLayout(boolean changed, int l, int t, int r, int b) {
186         super.onLayout(changed, l, t, r, b);
187         if (Utilities.ATLEAST_Q) {
188             for (int i = 0; i < HANDLE_COUNT; i++) {
189                 View dragHandle = mDragHandles[i];
190                 mSystemGestureExclusionRects.get(i).set(dragHandle.getLeft(), dragHandle.getTop(),
191                         dragHandle.getRight(), dragHandle.getBottom());
192             }
193             setSystemGestureExclusionRects(mSystemGestureExclusionRects);
194         }
195     }
196 
showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout)197     public static void showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout) {
198         Launcher launcher = Launcher.getLauncher(cellLayout.getContext());
199         AbstractFloatingView.closeAllOpenViews(launcher);
200 
201         DragLayer dl = launcher.getDragLayer();
202         AppWidgetResizeFrame frame = (AppWidgetResizeFrame) launcher.getLayoutInflater()
203                 .inflate(R.layout.app_widget_resize_frame, dl, false);
204         if (widget.hasEnforcedCornerRadius()) {
205             float enforcedCornerRadius = widget.getEnforcedCornerRadius();
206             ImageView imageView = frame.findViewById(R.id.widget_resize_frame);
207             Drawable d = imageView.getDrawable();
208             if (d instanceof GradientDrawable) {
209                 GradientDrawable gd = (GradientDrawable) d.mutate();
210                 gd.setCornerRadius(enforcedCornerRadius);
211             }
212         }
213         frame.setupForWidget(widget, cellLayout, dl);
214         ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true;
215 
216         dl.addView(frame);
217         frame.mIsOpen = true;
218         frame.post(() -> frame.snapToWidget(false));
219     }
220 
setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)221     private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout,
222             DragLayer dragLayer) {
223         mCellLayout = cellLayout;
224         if (mWidgetView != null) {
225             mWidgetView.removeOnAttachStateChangeListener(mWidgetViewAttachStateChangeListener);
226         }
227         mWidgetView = widgetView;
228         mWidgetView.addOnAttachStateChangeListener(mWidgetViewAttachStateChangeListener);
229         LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo)
230                 widgetView.getAppWidgetInfo();
231         mDragLayer = dragLayer;
232 
233         mMinHSpan = info.minSpanX;
234         mMinVSpan = info.minSpanY;
235         mMaxHSpan = info.maxSpanX;
236         mMaxVSpan = info.maxSpanY;
237 
238         mWidgetPadding = getDefaultPaddingForWidget(getContext(),
239                 widgetView.getAppWidgetInfo().provider, null);
240 
241         // Only show resize handles for the directions in which resizing is possible.
242         InvariantDeviceProfile idp = LauncherAppState.getIDP(cellLayout.getContext());
243         mVerticalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0
244                 && mMinVSpan < idp.numRows && mMaxVSpan > 1
245                 && mMinVSpan < mMaxVSpan;
246         if (!mVerticalResizeActive) {
247             mDragHandles[INDEX_TOP].setVisibility(GONE);
248             mDragHandles[INDEX_BOTTOM].setVisibility(GONE);
249         }
250         mHorizontalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0
251                 && mMinHSpan < idp.numColumns && mMaxHSpan > 1
252                 && mMinHSpan < mMaxHSpan;
253         if (!mHorizontalResizeActive) {
254             mDragHandles[INDEX_LEFT].setVisibility(GONE);
255             mDragHandles[INDEX_RIGHT].setVisibility(GONE);
256         }
257 
258         mReconfigureButton = (ImageButton) findViewById(R.id.widget_reconfigure_button);
259         if (info.isReconfigurable()) {
260             mReconfigureButton.setVisibility(VISIBLE);
261             mReconfigureButton.setOnClickListener(view -> {
262                 mLauncher.setWaitingForResult(
263                         PendingRequestArgs.forWidgetInfo(
264                                 mWidgetView.getAppWidgetId(),
265                                 // Widget add handler is null since we're reconfiguring an existing
266                                 // widget.
267                                 /* widgetHandler= */ null,
268                                 (ItemInfo) mWidgetView.getTag()));
269                 mLauncher
270                     .getAppWidgetHost()
271                     .startConfigActivity(
272                             mLauncher,
273                             mWidgetView.getAppWidgetId(),
274                             Launcher.REQUEST_RECONFIGURE_APPWIDGET);
275             });
276             if (!hasSeenReconfigurableWidgetEducationTip()) {
277                 post(() -> {
278                     if (showReconfigurableWidgetEducationTip() != null) {
279                         mLauncher.getSharedPrefs().edit()
280                                 .putBoolean(KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN,
281                                         true).apply();
282                     }
283                 });
284             }
285         }
286 
287         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams();
288         ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag();
289         lp.cellX = lp.tmpCellX = widgetInfo.cellX;
290         lp.cellY = lp.tmpCellY = widgetInfo.cellY;
291         lp.cellHSpan = widgetInfo.spanX;
292         lp.cellVSpan = widgetInfo.spanY;
293         lp.isLockedToGrid = true;
294 
295         // When we create the resize frame, we first mark all cells as unoccupied. The appropriate
296         // cells (same if not resized, or different) will be marked as occupied when the resize
297         // frame is dismissed.
298         mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
299 
300         mLauncher.getStatsLogManager()
301                 .logger()
302                 .withInstanceId(logInstanceId)
303                 .withItemInfo(widgetInfo)
304                 .log(LAUNCHER_WIDGET_RESIZE_STARTED);
305 
306         setOnKeyListener(this);
307     }
308 
309     public boolean beginResizeIfPointInRegion(int x, int y) {
310         mLeftBorderActive = (x < mTouchTargetWidth) && mHorizontalResizeActive;
311         mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && mHorizontalResizeActive;
312         mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment)
313                 && mVerticalResizeActive;
314         mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
315                 && mVerticalResizeActive;
316 
317         boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
318                 || mTopBorderActive || mBottomBorderActive;
319 
320         if (anyBordersActive) {
321             mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
322             mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA);
323             mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
324             mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
325         }
326 
327         if (mLeftBorderActive) {
328             mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth);
329         } else if (mRightBorderActive) {
330             mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight());
331         } else {
332             mDeltaXRange.set(0, 0);
333         }
334         mBaselineX.set(getLeft(), getRight());
335 
336         if (mTopBorderActive) {
337             mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth);
338         } else if (mBottomBorderActive) {
339             mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom());
340         } else {
341             mDeltaYRange.set(0, 0);
342         }
343         mBaselineY.set(getTop(), getBottom());
344 
345         return anyBordersActive;
346     }
347 
348     /**
349      *  Based on the deltas, we resize the frame.
350      */
351     public void visualizeResizeForDelta(int deltaX, int deltaY) {
352         mDeltaX = mDeltaXRange.clamp(deltaX);
353         mDeltaY = mDeltaYRange.clamp(deltaY);
354 
355         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
356         mDeltaX = mDeltaXRange.clamp(deltaX);
357         mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1);
358         lp.x = mTempRange1.start;
359         lp.width = mTempRange1.size();
360 
361         mDeltaY = mDeltaYRange.clamp(deltaY);
362         mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1);
363         lp.y = mTempRange1.start;
364         lp.height = mTempRange1.size();
365 
366         resizeWidgetIfNeeded(false);
367 
368         // When the widget resizes in multi-window mode, the translation value changes to maintain
369         // a center fit. These overrides ensure the resize frame always aligns with the widget view.
370         getSnappedRectRelativeToDragLayer(sTmpRect);
371         if (mLeftBorderActive) {
372             lp.width = sTmpRect.width() + sTmpRect.left - lp.x;
373         }
374         if (mTopBorderActive) {
375             lp.height = sTmpRect.height() + sTmpRect.top - lp.y;
376         }
377         if (mRightBorderActive) {
378             lp.x = sTmpRect.left;
379         }
380         if (mBottomBorderActive) {
381             lp.y = sTmpRect.top;
382         }
383 
384         // Handle invalid resize across CellLayouts in the two panel UI.
385         if (mCellLayout.getParent() instanceof Workspace) {
386             Workspace workspace = (Workspace) mCellLayout.getParent();
387             CellLayout pairedCellLayout = workspace.getScreenPair(mCellLayout);
388             if (pairedCellLayout != null) {
389                 Rect focusedCellLayoutBound = sTmpRect;
390                 mDragLayerRelativeCoordinateHelper.viewToRect(mCellLayout, focusedCellLayoutBound);
391                 Rect resizeFrameBound = sTmpRect2;
392                 findViewById(R.id.widget_resize_frame).getGlobalVisibleRect(resizeFrameBound);
393                 float progress = 1f;
394                 if (workspace.indexOfChild(pairedCellLayout) < workspace.indexOfChild(mCellLayout)
395                         && mDeltaX < 0
396                         && resizeFrameBound.left < focusedCellLayoutBound.left) {
397                     // Resize from right to left.
398                     progress = (mDragAcrossTwoPanelOpacityMargin + mDeltaX)
399                             / mDragAcrossTwoPanelOpacityMargin;
400                 } else if (workspace.indexOfChild(pairedCellLayout)
401                                 > workspace.indexOfChild(mCellLayout)
402                         && mDeltaX > 0
403                         && resizeFrameBound.right > focusedCellLayoutBound.right) {
404                     // Resize from left to right.
405                     progress = (mDragAcrossTwoPanelOpacityMargin - mDeltaX)
406                             / mDragAcrossTwoPanelOpacityMargin;
407                 }
408                 float alpha = Math.max(MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE, progress);
409                 float springLoadedProgress = Math.min(1f, 1f - progress);
410                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, alpha,
411                         springLoadedProgress);
412             }
413         }
414 
415         requestLayout();
416     }
417 
418     private static int getSpanIncrement(float deltaFrac) {
419         return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0;
420     }
421 
422     /**
423      *  Based on the current deltas, we determine if and how to resize the widget.
424      */
425     private void resizeWidgetIfNeeded(boolean onDismiss) {
426         DeviceProfile dp = mLauncher.getDeviceProfile();
427         float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x;
428         float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y;
429 
430         int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
431         int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
432 
433         if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return;
434 
435         mDirectionVector[0] = 0;
436         mDirectionVector[1] = 0;
437 
438         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams();
439 
440         int spanX = lp.cellHSpan;
441         int spanY = lp.cellVSpan;
442         int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX;
443         int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY;
444 
445         // For each border, we bound the resizing based on the minimum width, and the maximum
446         // expandability.
447         mTempRange1.set(cellX, spanX + cellX);
448         int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive,
449                 hSpanInc, mMinHSpan, mMaxHSpan, mCellLayout.getCountX(), mTempRange2);
450         cellX = mTempRange2.start;
451         spanX = mTempRange2.size();
452         if (hSpanDelta != 0) {
453             mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
454         }
455 
456         mTempRange1.set(cellY, spanY + cellY);
457         int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive,
458                 vSpanInc, mMinVSpan, mMaxVSpan, mCellLayout.getCountY(), mTempRange2);
459         cellY = mTempRange2.start;
460         spanY = mTempRange2.size();
461         if (vSpanDelta != 0) {
462             mDirectionVector[1] = mTopBorderActive ? -1 : 1;
463         }
464 
465         if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
466 
467         // We always want the final commit to match the feedback, so we make sure to use the
468         // last used direction vector when committing the resize / reorder.
469         if (onDismiss) {
470             mDirectionVector[0] = mLastDirectionVector[0];
471             mDirectionVector[1] = mLastDirectionVector[1];
472         } else {
473             mLastDirectionVector[0] = mDirectionVector[0];
474             mLastDirectionVector[1] = mDirectionVector[1];
475         }
476 
477         if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
478                 mDirectionVector, onDismiss)) {
479             if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) {
480                 mStateAnnouncer.announce(
481                         mLauncher.getString(R.string.widget_resized, spanX, spanY));
482             }
483 
484             lp.tmpCellX = cellX;
485             lp.tmpCellY = cellY;
486             lp.cellHSpan = spanX;
487             lp.cellVSpan = spanY;
488             mRunningVInc += vSpanDelta;
489             mRunningHInc += hSpanDelta;
490 
491             if (!onDismiss) {
492                 WidgetSizes.updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
493             }
494         }
495         mWidgetView.requestLayout();
496     }
497 
498     @Override
499     protected void onDetachedFromWindow() {
500         super.onDetachedFromWindow();
501 
502         // We are done with resizing the widget. Save the widget size & position to LauncherModel
503         resizeWidgetIfNeeded(true);
504         mLauncher.getStatsLogManager()
505                 .logger()
506                 .withInstanceId(logInstanceId)
507                 .withItemInfo((ItemInfo) mWidgetView.getTag())
508                 .log(LAUNCHER_WIDGET_RESIZE_COMPLETED);
509     }
510 
511     private void onTouchUp() {
512         DeviceProfile dp = mLauncher.getDeviceProfile();
513         int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x;
514         int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y;
515 
516         mDeltaXAddOn = mRunningHInc * xThreshold;
517         mDeltaYAddOn = mRunningVInc * yThreshold;
518         mDeltaX = 0;
519         mDeltaY = 0;
520 
521         post(() -> snapToWidget(true));
522     }
523 
524     /**
525      * Returns the rect of this view when the frame is snapped around the widget, with the bounds
526      * relative to the {@link DragLayer}.
527      */
528     private void getSnappedRectRelativeToDragLayer(Rect out) {
529         float scale = mWidgetView.getScaleToFit();
530 
531         mDragLayer.getViewRectRelativeToSelf(mWidgetView, out);
532 
533         int width = 2 * mBackgroundPadding
534                 + (int) (scale * (out.width() - mWidgetPadding.left - mWidgetPadding.right));
535         int height = 2 * mBackgroundPadding
536                 + (int) (scale * (out.height() - mWidgetPadding.top - mWidgetPadding.bottom));
537 
538         int x = (int) (out.left - mBackgroundPadding + scale * mWidgetPadding.left);
539         int y = (int) (out.top - mBackgroundPadding + scale * mWidgetPadding.top);
540 
541         out.left = x;
542         out.top = y;
543         out.right = out.left + width;
544         out.bottom = out.top + height;
545     }
546 
547     private void snapToWidget(boolean animate) {
548         getSnappedRectRelativeToDragLayer(sTmpRect);
549         int newWidth = sTmpRect.width();
550         int newHeight = sTmpRect.height();
551         int newX = sTmpRect.left;
552         int newY = sTmpRect.top;
553 
554         // We need to make sure the frame's touchable regions lie fully within the bounds of the
555         // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions
556         // down accordingly to provide a proper touch target.
557         if (newY < 0) {
558             // In this case we shift the touch region down to start at the top of the DragLayer
559             mTopTouchRegionAdjustment = -newY;
560         } else {
561             mTopTouchRegionAdjustment = 0;
562         }
563         if (newY + newHeight > mDragLayer.getHeight()) {
564             // In this case we shift the touch region up to end at the bottom of the DragLayer
565             mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
566         } else {
567             mBottomTouchRegionAdjustment = 0;
568         }
569 
570         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
571         final CellLayout pairedCellLayout;
572         if (mCellLayout.getParent() instanceof Workspace) {
573             Workspace workspace = (Workspace) mCellLayout.getParent();
574             pairedCellLayout = workspace.getScreenPair(mCellLayout);
575         } else {
576             pairedCellLayout = null;
577         }
578         if (!animate) {
579             lp.width = newWidth;
580             lp.height = newHeight;
581             lp.x = newX;
582             lp.y = newY;
583             for (int i = 0; i < HANDLE_COUNT; i++) {
584                 mDragHandles[i].setAlpha(1f);
585             }
586             if (pairedCellLayout != null) {
587                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
588                         /* springLoadedProgress= */ 0f);
589             }
590             requestLayout();
591         } else {
592             ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp,
593                     PropertyValuesHolder.ofInt(LAYOUT_WIDTH, lp.width, newWidth),
594                     PropertyValuesHolder.ofInt(LAYOUT_HEIGHT, lp.height, newHeight),
595                     PropertyValuesHolder.ofInt(LAYOUT_X, lp.x, newX),
596                     PropertyValuesHolder.ofInt(LAYOUT_Y, lp.y, newY));
597             mFirstFrameAnimatorHelper.addTo(oa).addUpdateListener(a -> requestLayout());
598 
599             AnimatorSet set = new AnimatorSet();
600             set.play(oa);
601             for (int i = 0; i < HANDLE_COUNT; i++) {
602                 set.play(mFirstFrameAnimatorHelper.addTo(
603                         ObjectAnimator.ofFloat(mDragHandles[i], ALPHA, 1f)));
604             }
605             if (pairedCellLayout != null) {
606                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
607                         /* springLoadedProgress= */ 0f, /* animatorSet= */ set);
608             }
609             set.setDuration(SNAP_DURATION);
610             set.start();
611         }
612 
613         setFocusableInTouchMode(true);
614         requestFocus();
615     }
616 
617     @Override
618     public boolean onKey(View v, int keyCode, KeyEvent event) {
619         // Clear the frame and give focus to the widget host view when a directional key is pressed.
620         if (shouldConsume(keyCode)) {
621             close(false);
622             mWidgetView.requestFocus();
623             return true;
624         }
625         return false;
626     }
627 
628     private boolean handleTouchDown(MotionEvent ev) {
629         Rect hitRect = new Rect();
630         int x = (int) ev.getX();
631         int y = (int) ev.getY();
632 
633         getHitRect(hitRect);
634         if (hitRect.contains(x, y)) {
635             if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) {
636                 mXDown = x;
637                 mYDown = y;
638                 return true;
639             }
640         }
641         return false;
642     }
643 
644     private boolean isTouchOnReconfigureButton(MotionEvent ev) {
645         int xFrame = (int) ev.getX() - getLeft();
646         int yFrame = (int) ev.getY() - getTop();
647         mReconfigureButton.getHitRect(sTmpRect);
648         return sTmpRect.contains(xFrame, yFrame);
649     }
650 
651     @Override
652     public boolean onControllerTouchEvent(MotionEvent ev) {
653         int action = ev.getAction();
654         int x = (int) ev.getX();
655         int y = (int) ev.getY();
656 
657         switch (action) {
658             case MotionEvent.ACTION_DOWN:
659                 return handleTouchDown(ev);
660             case MotionEvent.ACTION_MOVE:
661                 visualizeResizeForDelta(x - mXDown, y - mYDown);
662                 break;
663             case MotionEvent.ACTION_CANCEL:
664             case MotionEvent.ACTION_UP:
665                 visualizeResizeForDelta(x - mXDown, y - mYDown);
666                 onTouchUp();
667                 mXDown = mYDown = 0;
668                 break;
669         }
670         return true;
671     }
672 
673     @Override
674     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
675         if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) {
676             return true;
677         }
678         // Keep the resize frame open but let a click on the reconfigure button fall through to the
679         // button's OnClickListener.
680         if (isTouchOnReconfigureButton(ev)) {
681             return false;
682         }
683         close(false);
684         return false;
685     }
686 
687     @Override
688     protected void handleClose(boolean animate) {
689         mDragLayer.removeView(this);
690         if (mWidgetView != null) {
691             mWidgetView.removeOnAttachStateChangeListener(mWidgetViewAttachStateChangeListener);
692         }
693     }
694 
695     private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout,
696             float alpha, float springLoadedProgress) {
697         updateInvalidResizeEffect(cellLayout, pairedCellLayout, alpha,
698                 springLoadedProgress, /* animatorSet= */ null);
699     }
700 
701     private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout,
702             float alpha, float springLoadedProgress, @Nullable AnimatorSet animatorSet) {
703         int childCount = pairedCellLayout.getChildCount();
704         for (int i = 0; i < childCount; i++) {
705             View child = pairedCellLayout.getChildAt(i);
706             if (animatorSet != null) {
707                 animatorSet.play(
708                         mFirstFrameAnimatorHelper.addTo(
709                                 ObjectAnimator.ofFloat(child, ALPHA, alpha)));
710             } else {
711                 child.setAlpha(alpha);
712             }
713         }
714         if (animatorSet != null) {
715             animatorSet.play(mFirstFrameAnimatorHelper.addTo(
716                     ObjectAnimator.ofFloat(cellLayout, SPRING_LOADED_PROGRESS,
717                             springLoadedProgress)));
718             animatorSet.play(mFirstFrameAnimatorHelper.addTo(
719                     ObjectAnimator.ofFloat(pairedCellLayout, SPRING_LOADED_PROGRESS,
720                             springLoadedProgress)));
721         } else {
722             cellLayout.setSpringLoadedProgress(springLoadedProgress);
723             pairedCellLayout.setSpringLoadedProgress(springLoadedProgress);
724         }
725 
726         boolean shouldShowCellLayoutBorder = springLoadedProgress > 0f;
727         if (animatorSet != null) {
728             animatorSet.addListener(new AnimatorListenerAdapter() {
729                 @Override
730                 public void onAnimationEnd(Animator animator) {
731                     cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
732                     pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
733                 }
734             });
735         } else {
736             cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
737             pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
738         }
739     }
740 
741     @Override
742     protected boolean isOfType(int type) {
743         return (type & TYPE_WIDGET_RESIZE_FRAME) != 0;
744     }
745 
746     /**
747      * A mutable class for describing the range of two int values.
748      */
749     private static class IntRange {
750 
751         public int start, end;
752 
753         public int clamp(int value) {
754             return Utilities.boundToRange(value, start, end);
755         }
756 
757         public void set(int s, int e) {
758             start = s;
759             end = e;
760         }
761 
762         public int size() {
763             return end - start;
764         }
765 
766         /**
767          * Moves either the start or end edge (but never both) by {@param delta} and  sets the
768          * result in {@param out}
769          */
770         public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) {
771             out.start = moveStart ? start + delta : start;
772             out.end = moveEnd ? end + delta : end;
773         }
774 
775         /**
776          * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)},
777          * with extra conditions.
778          * @param minSize minimum size after with the moving edge should not be shifted any further.
779          *                For eg, if delta = -3 when moving the endEdge brings the size to less than
780          *                minSize, only delta = -2 will applied
781          * @param maxSize maximum size after with the moving edge should not be shifted any further.
782          *                For eg, if delta = -3 when moving the endEdge brings the size to greater
783          *                than maxSize, only delta = -2 will applied
784          * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0)
785          * @return the amount of increase when endEdge was moves and the amount of decrease when
786          * the start edge was moved.
787          */
788         public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta,
789                 int minSize, int maxSize, int maxEnd, IntRange out) {
790             applyDelta(moveStart, moveEnd, delta, out);
791             if (out.start < 0) {
792                 out.start = 0;
793             }
794             if (out.end > maxEnd) {
795                 out.end = maxEnd;
796             }
797             if (out.size() < minSize) {
798                 if (moveStart) {
799                     out.start = out.end - minSize;
800                 } else if (moveEnd) {
801                     out.end = out.start + minSize;
802                 }
803             }
804             if (out.size() > maxSize) {
805                 if (moveStart) {
806                     out.start = out.end - maxSize;
807                 } else if (moveEnd) {
808                     out.end = out.start + maxSize;
809                 }
810             }
811             return moveEnd ? out.size() - size() : size() - out.size();
812         }
813     }
814 
815     /**
816      * Returns true only if this utility class handles the key code.
817      */
818     public static boolean shouldConsume(int keyCode) {
819         return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
820                 || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN
821                 || keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END
822                 || keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN);
823     }
824 
825     @Nullable private ArrowTipView showReconfigurableWidgetEducationTip() {
826         Rect rect = new Rect();
827         if (!mReconfigureButton.getGlobalVisibleRect(rect)) {
828             return null;
829         }
830         @Px int tipMargin = mLauncher.getResources()
831                 .getDimensionPixelSize(R.dimen.widget_reconfigure_tip_top_margin);
832         return new ArrowTipView(mLauncher, /* isPointingUp= */ true)
833                 .showAroundRect(
834                         getContext().getString(R.string.reconfigurable_widget_education_tip),
835                         /* arrowXCoord= */ rect.left + mReconfigureButton.getWidth() / 2,
836                         /* rect= */ rect,
837                         /* margin= */ tipMargin);
838     }
839 
840     private boolean hasSeenReconfigurableWidgetEducationTip() {
841         return mLauncher.getSharedPrefs()
842                 .getBoolean(KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN, false)
843                 || Utilities.IS_RUNNING_IN_TEST_HARNESS;
844     }
845 }
846