1 /*
2  * Copyright (C) 2023 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 
17 package com.android.car.carlauncher.recyclerview;
18 
19 import static android.view.View.DRAG_FLAG_GLOBAL;
20 import static android.view.View.DRAG_FLAG_OPAQUE;
21 
22 import static com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection;
23 import static com.android.car.carlauncher.AppGridConstants.PageOrientation;
24 import static com.android.car.carlauncher.AppGridConstants.isHorizontal;
25 import static com.android.car.hidden.apis.HiddenApiAccess.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION;
26 
27 import android.content.ClipData;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.graphics.Point;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.graphics.drawable.TransitionDrawable;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.util.Pair;
37 import android.view.DragEvent;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.ViewPropertyAnimator;
41 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
42 import android.widget.ImageView;
43 import android.widget.LinearLayout;
44 import android.widget.TextView;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 import androidx.recyclerview.widget.RecyclerView;
49 
50 import com.android.car.carlauncher.AppGridActivity;
51 import com.android.car.carlauncher.AppGridPageSnapper.AppGridPageSnapCallback;
52 import com.android.car.carlauncher.AppItemDragShadowBuilder;
53 import com.android.car.carlauncher.AppMetaData;
54 import com.android.car.carlauncher.R;
55 import com.android.car.carlaunchercommon.toasts.NonDrivingOptimizedLaunchFailedToast;
56 
57 /**
58  * App item view holder that contains the app icon and name.
59  */
60 public class AppItemViewHolder extends RecyclerView.ViewHolder {
61     private static final String APP_ITEM_DRAG_TAG = "com.android.car.launcher.APP_ITEM_DRAG_TAG";
62     private final long mReleaseAnimationDurationMs;
63     private final long mLongPressAnimationDurationMs;
64     private final long mDropAnimationDelayMs;
65     private final int mHighlightTransitionDurationMs;
66     private final int mIconSize;
67     private final int mIconScaledSize;
68     private final Context mContext;
69     private final LinearLayout mAppItemView;
70     private final ImageView mAppIcon;
71     private final TextView mAppName;
72     private final AppItemDragCallback mDragCallback;
73     private final AppGridPageSnapCallback mSnapCallback;
74     private final boolean mConfigReorderAllowed;
75     private final int mThresholdToStartDragDrop;
76     private Rect mPageBound;
77 
78     @PageOrientation
79     private int mPageOrientation;
80     @AppItemBoundDirection
81     private int mDragExitDirection;
82 
83     private boolean mHasAppMetadata;
84     private ComponentName mComponentName;
85     private Point mAppIconCenter;
86     private TransitionDrawable mBackgroundHighlight;
87     private int mAppItemWidth;
88     private int mAppItemHeight;
89     private boolean mIsTargeted;
90     private boolean mCanStartDragAction;
91 
92     /**
93      * Information describing state of the recyclerview when this view holder was last rebinded.
94      *
95      * {@param isDistractionOptimizationRequired} true if driving restriction should be required.
96      * {@param pageBound} the bounds of the recyclerview containing this view holder.
97      */
98     public static class BindInfo {
99         private final boolean mIsDistractionOptimizationRequired;
100         private final Rect mPageBound;
101         private final AppGridActivity.Mode mMode;
102 
BindInfo(boolean isDistractionOptimizationRequired, Rect pageBound, AppGridActivity.Mode mode)103         public BindInfo(boolean isDistractionOptimizationRequired,
104                 Rect pageBound,
105                 AppGridActivity.Mode mode) {
106             this.mIsDistractionOptimizationRequired = isDistractionOptimizationRequired;
107             this.mPageBound = pageBound;
108             this.mMode = mode;
109         }
110 
BindInfo(boolean isDistractionOptimizationRequired, Rect pageBound)111         public BindInfo(boolean isDistractionOptimizationRequired, Rect pageBound) {
112             this(isDistractionOptimizationRequired, pageBound, AppGridActivity.Mode.ALL_APPS);
113         }
114     }
115 
AppItemViewHolder(View view, Context context, AppItemDragCallback dragCallback, AppGridPageSnapCallback snapCallback)116     public AppItemViewHolder(View view, Context context, AppItemDragCallback dragCallback,
117             AppGridPageSnapCallback snapCallback) {
118         super(view);
119         mContext = context;
120         mAppItemView = view.findViewById(R.id.app_item);
121         mAppIcon = mAppItemView.findViewById(R.id.app_icon);
122         mAppName = mAppItemView.findViewById(R.id.app_name);
123         mDragCallback = dragCallback;
124         mSnapCallback = snapCallback;
125 
126         mIconSize = context.getResources().getDimensionPixelSize(R.dimen.app_icon_size);
127         mConfigReorderAllowed = context.getResources().getBoolean(R.bool.config_allow_reordering);
128         // distance that users must drag (hold and attempt to move the app icon) to initiate
129         // reordering, measured in pixels on screen.
130         mThresholdToStartDragDrop = context.getResources().getDimensionPixelSize(
131                 R.dimen.threshold_to_start_drag_drop);
132         mPageOrientation = context.getResources().getBoolean(R.bool.use_vertical_app_grid)
133                 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL;
134 
135         mIconScaledSize = context.getResources().getDimensionPixelSize(
136                 R.dimen.app_icon_scaled_size);
137         // duration for animating the resizing of app icon on long press
138         mLongPressAnimationDurationMs = context.getResources().getInteger(
139                 R.integer.ms_long_press_animation_duration);
140         // duration for animating the resizing after long press is released
141         mReleaseAnimationDurationMs = context.getResources().getInteger(
142                 R.integer.ms_release_animation_duration);
143         // duration to animate the highlighting of view holder when it is targeted during drag drop
144         mHighlightTransitionDurationMs = context.getResources().getInteger(
145                 R.integer.ms_background_highlight_duration);
146         // delay before animating the drop animation when a valid drop event has been received
147         mDropAnimationDelayMs = context.getResources().getInteger(
148                 R.integer.ms_drop_animation_delay);
149     }
150 
151     /**
152      * Binds the grid app item view with the app metadata.
153      *
154      * @param app AppMetaData to be displayed. Pass {@code null} will empty out the viewHolder.
155      */
bind(@ullable AppMetaData app, @NonNull BindInfo bindInfo)156     public void bind(@Nullable AppMetaData app, @NonNull BindInfo bindInfo) {
157         resetViewHolder();
158         if (app == null) {
159             return;
160         }
161         boolean isDistractionOptimizationRequired = bindInfo.mIsDistractionOptimizationRequired;
162         mPageBound = bindInfo.mPageBound;
163         AppGridActivity.Mode mode = bindInfo.mMode;
164 
165         mHasAppMetadata = true;
166         mAppItemView.setFocusable(true);
167         mAppName.setText(app.getDisplayName());
168         mAppIcon.setImageDrawable(app.getIcon());
169         mAppIcon.setAlpha(1.f);
170         mComponentName = app.getComponentName();
171 
172         Drawable highlightedLayer = mContext.getDrawable(R.drawable.app_item_highlight);
173         Drawable emptyLayer = mContext.getDrawable(R.drawable.app_item_highlight);
174         emptyLayer.setAlpha(0);
175         mBackgroundHighlight = new TransitionDrawable(new Drawable[]{emptyLayer, highlightedLayer});
176         mBackgroundHighlight.resetTransition();
177         mAppItemView.setBackground(mBackgroundHighlight);
178 
179         // app icon's relative location within view holders are only measurable after it is drawn
180 
181         // during a drag and drop operation, the user could scroll to another page and return to the
182         // previous page, so we need to rebind the app with the correct visibility.
183         setStateSelected(mComponentName.equals(mDragCallback.mSelectedComponent));
184 
185         boolean isLaunchableDistractionOptimized = !isDistractionOptimizationRequired
186                 || app.getIsDistractionOptimized();
187         boolean isDisabledByTos = app.getIsDisabledByTos();
188         boolean isLaunchable = isLaunchableDistractionOptimized || isDisabledByTos;
189 
190         int opacity = getOpacity(isLaunchable, isDisabledByTos);
191         mAppIcon.setAlpha(mContext.getResources().getFloat(opacity));
192 
193         if (isLaunchable) {
194             View.OnClickListener appLaunchListener = new View.OnClickListener() {
195                 @Override
196                 public void onClick(View v) {
197                     app.getLaunchCallback().accept(mContext);
198                     mSnapCallback.notifySnapToPosition(getAbsoluteAdapterPosition());
199                 }
200             };
201             mAppItemView.setOnClickListener(appLaunchListener);
202             mAppIcon.setOnClickListener(appLaunchListener);
203             // long click actions should not be enabled when driving
204             if (!isDistractionOptimizationRequired) {
205                 View.OnLongClickListener longPressListener = new View.OnLongClickListener() {
206                     @Override
207                     public boolean onLongClick(View v) {
208                         // display set shortcut pop-up for force stop
209                         app.getAlternateLaunchCallback().accept(Pair.create(mContext, v));
210                         // drag and drop should only start after long click animation is complete
211                         mDragCallback.notifyItemLongPressed(true);
212                         mDragCallback.scheduleDragTask(new Runnable() {
213                             @Override
214                             public void run() {
215                                 mCanStartDragAction = true;
216                             }
217                         }, mLongPressAnimationDurationMs);
218                         animateIconResize(/* scale */ ((float) mIconScaledSize / mIconSize),
219                                 /* duration */ mLongPressAnimationDurationMs);
220                         return true;
221                     }
222                 };
223                 mAppIcon.setLongClickable(true);
224                 mAppIcon.setOnLongClickListener(longPressListener);
225                 mAppIcon.setOnTouchListener(new View.OnTouchListener() {
226                     private float mActionDownX;
227                     private float mActionDownY;
228 
229                     @Override
230                     public boolean onTouch(View v, MotionEvent event) {
231                         int action = event.getAction();
232                         if (action == MotionEvent.ACTION_DOWN) {
233                             mActionDownX = event.getX();
234                             mActionDownY = event.getY();
235                             mCanStartDragAction = false;
236                         } else if (action == MotionEvent.ACTION_MOVE
237                                 && shouldStartDragAndDrop(event,
238                                 mActionDownX,
239                                 mActionDownY,
240                                 mode)) {
241                             startDragAndDrop(app, event.getX(), event.getY());
242                             mCanStartDragAction = false;
243                         } else if (action == MotionEvent.ACTION_UP
244                                 || action == MotionEvent.ACTION_CANCEL) {
245                             animateIconResize(/* scale */ 1.f,
246                                     /* duration */ mReleaseAnimationDurationMs);
247                             mDragCallback.cancelDragTasks();
248                             mDragCallback.notifyItemLongPressed(false);
249                             mCanStartDragAction = false;
250                         }
251                         return false;
252                     }
253                 });
254             }
255         } else {
256             View.OnClickListener appLaunchListener = v ->
257                     NonDrivingOptimizedLaunchFailedToast.Companion.showToast(
258                             mContext, app.getDisplayName());
259             mAppItemView.setOnClickListener(appLaunchListener);
260             mAppIcon.setOnClickListener(appLaunchListener);
261 
262             mAppIcon.setLongClickable(false);
263             mAppIcon.setOnLongClickListener(null);
264             mAppIcon.setOnTouchListener(null);
265         }
266     }
267 
animateIconResize(float scale, long duration)268     void animateIconResize(float scale, long duration) {
269         mAppIcon.animate().setDuration(duration).scaleX(scale);
270         mAppIcon.animate().setDuration(duration).scaleY(scale);
271     }
272 
273     /**
274      * Transforms the app icon into the drop shadow's drop location in preparation for animateDrop,
275      * which should be dispatched by AppGridItemAnimator shortly after prepareForDropAnimation.
276      */
prepareForDropAnimation()277     public void prepareForDropAnimation() {
278         // dragOffset is the offset between dragged icon center and users finger touch point
279         int dragOffsetX = mDragCallback.mDragPoint.x - mIconScaledSize / 2;
280         int dragOffsetY = mDragCallback.mDragPoint.y - mIconScaledSize / 2;
281         // draggedIconCenter is the center of the dropped app icon, after the user finger touch
282         // point offset is subtracted to another
283         int draggedIconCenterX = mDragCallback.mDropPoint.x - dragOffsetX;
284         int draggedIconCenterY = mDragCallback.mDropPoint.y - dragOffsetY;
285         // dx and dx are the offset to translate between the dragged icon and dropped location
286         int dx = draggedIconCenterX - mDragCallback.mDropDestination.x;
287         int dy = draggedIconCenterY - mDragCallback.mDropDestination.y;
288         mAppIcon.setScaleX((float) mIconScaledSize / mIconSize);
289         mAppIcon.setScaleY((float) mIconScaledSize / mIconSize);
290         mAppIcon.setAlpha(1.f);
291         mAppIcon.setTranslationX(dx);
292         mAppIcon.setTranslationY(dy);
293         mAppItemView.setTranslationZ(.5f);
294         mAppName.setTranslationZ(.5f);
295         mAppIcon.setTranslationZ(1.f);
296     }
297 
298     /**
299      * Resets Z axis translation of all views contained by the view holder.
300      */
resetTranslationZ()301     public void resetTranslationZ() {
302         mAppItemView.setTranslationZ(0.f);
303         mAppIcon.setTranslationZ(0.f);
304         mAppName.setTranslationZ(0.f);
305     }
306 
307     /**
308      * Animates the drop transition back to the original app icon location.
309      */
getDropAnimation()310     public ViewPropertyAnimator getDropAnimation() {
311         return mAppIcon.animate()
312                 .translationX(0).translationY(0)
313                 .scaleX(1.f).scaleY(1.f)
314                 .setStartDelay(mDropAnimationDelayMs);
315     }
316 
resetViewHolder()317     private void resetViewHolder() {
318         // TODO: Create a different item for empty app item.
319         mHasAppMetadata = false;
320 
321         mAppItemView.setOnDragListener(new AppItemOnDragListener());
322         mAppItemView.setFocusable(false);
323         mAppItemView.setOnClickListener(null);
324 
325         mAppIcon.setLongClickable(false);
326         mAppIcon.setOnLongClickListener(null);
327         mAppIcon.setOnTouchListener(null);
328         mAppIcon.setAlpha(0.f);
329         mAppIcon.setOutlineProvider(null);
330 
331         mAppIcon.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
332             @Override
333             public void onGlobalLayout() {
334                 // remove listener since icon only need to be measured once
335                 mAppIcon.getViewTreeObserver().removeOnGlobalLayoutListener(this);
336                 Rect appIconBound = new Rect();
337                 mAppIcon.getDrawingRect(appIconBound);
338                 mAppItemView.offsetDescendantRectToMyCoords(mAppIcon, appIconBound);
339                 mAppIconCenter = new Point(/* x */ (appIconBound.right + appIconBound.left) / 2,
340                         /* y */ (appIconBound.bottom + appIconBound.top) / 2);
341                 mAppItemWidth = mAppItemView.getWidth();
342                 mAppItemHeight = mAppItemView.getHeight();
343             }
344         });
345 
346         mAppItemView.setBackground(null);
347         mAppIcon.setImageDrawable(null);
348         mAppName.setText(null);
349 
350         mDragExitDirection = AppItemBoundDirection.NONE;
351     }
352 
setStateTargeted(boolean targeted)353     private void setStateTargeted(boolean targeted) {
354         if (mIsTargeted == targeted) return;
355         mIsTargeted = targeted;
356         if (targeted) {
357             mDragCallback.notifyItemTargeted(AppItemViewHolder.this);
358             mBackgroundHighlight.startTransition(mHighlightTransitionDurationMs);
359             return;
360         }
361         mDragCallback.notifyItemTargeted(null);
362         mBackgroundHighlight.resetTransition();
363     }
364 
setStateSelected(boolean selected)365     private void setStateSelected(boolean selected) {
366         if (selected) {
367             mAppIcon.setAlpha(0.f);
368             return;
369         }
370         if (mHasAppMetadata) {
371             mAppIcon.setAlpha(1.f);
372         }
373     }
374 
375 
shouldStartDragAndDrop(MotionEvent event, float actionDownX, float actionDownY, AppGridActivity.Mode mode)376     private boolean shouldStartDragAndDrop(MotionEvent event, float actionDownX,
377             float actionDownY, AppGridActivity.Mode mode) {
378         // If App Grid is not in all apps mode, we should not allow drag and drop
379         if (mode != AppGridActivity.Mode.ALL_APPS) {
380             return false;
381         }
382         // the move event should be with in the bounds of the app icon
383         boolean isEventWithinIcon = event.getX() >= 0 && event.getY() >= 0
384                 && event.getX() < mIconScaledSize && event.getY() < mIconScaledSize;
385         // the move event should be further by more than mThresholdToStartDragDrop pixels
386         // away from the initial touch input.
387         boolean isDistancePastThreshold = Math.hypot(/* dx */ Math.abs(event.getX() - actionDownX),
388                 /* dy */ event.getY() - actionDownY) > mThresholdToStartDragDrop;
389         return mConfigReorderAllowed && mCanStartDragAction && isEventWithinIcon
390                 && isDistancePastThreshold;
391     }
392 
393     private void startDragAndDrop(AppMetaData app, float eventX, float eventY) {
394         ClipData clipData = ClipData.newPlainText(/* label= */ APP_ITEM_DRAG_TAG,
395                 /* text= */ app.getComponentName().flattenToString());
396 
397         // since the app icon is scaled, the touch point that users should be holding when drag
398         // shadow is deployed should also be scaled
399         Point dragPoint = new Point(/* x */ (int) (eventX / mIconSize * mIconScaledSize),
400                 /* y */ (int) (eventY / mIconSize * mIconScaledSize));
401 
402         Drawable appIcon = app.getIcon();
403         if (appIcon.getConstantState() == null) return;
404         AppItemDragShadowBuilder dragShadowBuilder = new AppItemDragShadowBuilder(
405                 appIcon.getConstantState().newDrawable().mutate(),
406                 /* touchPointX */ dragPoint.x, /* touchPointX */ dragPoint.y,
407                 /* scaledSize */ mIconScaledSize);
408         mAppIcon.startDragAndDrop(clipData, /* dragShadowBuilder */ dragShadowBuilder,
409                 /* myLocalState */ null, /* flags */ DRAG_FLAG_OPAQUE | DRAG_FLAG_GLOBAL
410                         | DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
411 
412         mDragCallback.notifyItemSelected(AppItemViewHolder.this, dragPoint);
413     }
414 
415     class AppItemOnDragListener implements View.OnDragListener {
416         @Override
417         public boolean onDrag(View view, DragEvent event) {
418             int action = event.getAction();
419             if (mHasAppMetadata) {
420                 if (action == DragEvent.ACTION_DRAG_STARTED) {
421                     if (isSelectedViewHolder()) {
422                         setStateSelected(true);
423                     }
424                 } else if (action == DragEvent.ACTION_DRAG_LOCATION && inScrollStateIdle()) {
425                     boolean shouldTargetViewHolder = isTargetIconVisible()
426                             && isDraggedIconInBound(event)
427                             && mDragCallback.mSelectedComponent != null;
428                     setStateTargeted(shouldTargetViewHolder);
429                 } else if (action == DragEvent.ACTION_DRAG_EXITED && inScrollStateIdle()) {
430                     setStateTargeted(false);
431                 } else if (action == DragEvent.ACTION_DROP) {
432                     if (isTargetedViewHolder()) {
433                         Point dropPoint = new Point(/* x */ (int) event.getX(),
434                                 /* y */ (int) event.getY());
435                         mDragCallback.notifyItemDropped(dropPoint);
436                     }
437                     setStateTargeted(false);
438                 }
439             }
440             if (action == DragEvent.ACTION_DRAG_ENTERED && inScrollStateIdle()) {
441                 mDragCallback.notifyItemDragged();
442             }
443             if (action == DragEvent.ACTION_DRAG_LOCATION && inScrollStateIdle()) {
444                 mDragExitDirection = getClosestBoundDirection(event.getX(), event.getY());
445                 mDragCallback.notifyItemDragged();
446             }
447             if (action == DragEvent.ACTION_DRAG_EXITED && inScrollStateIdle()) {
448                 mDragCallback.notifyDragExited(AppItemViewHolder.this, mDragExitDirection);
449                 mDragExitDirection = AppItemBoundDirection.NONE;
450             }
451             if (event.getAction() == DragEvent.ACTION_DRAG_ENDED) {
452                 mDragExitDirection = AppItemBoundDirection.NONE;
453                 setStateSelected(false);
454             }
455             if (action == DragEvent.ACTION_DROP) {
456                 return false;
457             }
458             return true;
459         }
460     }
461 
462     private boolean isSelectedViewHolder() {
463         return mComponentName != null && mComponentName.equals(mDragCallback.mSelectedComponent);
464     }
465 
466     private boolean isTargetedViewHolder() {
467         return mComponentName != null && mComponentName.equals(mDragCallback.mTargetedComponent);
468     }
469 
470     private boolean inScrollStateIdle() {
471         return mSnapCallback.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
472     }
473 
474     /**
475      * Returns whether this view holder's icon is visible to the user.
476      *
477      * Since the edge of the view holder from the previous/next may also receive drop events, a
478      * valid drop target should have its app icon be visible to the user.
479      */
480     private boolean isTargetIconVisible() {
481         if (mAppIcon == null || mAppIcon.getMeasuredWidth() == 0) {
482             return false;
483         }
484         final Rect bound = new Rect();
485         mAppIcon.getGlobalVisibleRect(bound);
486         return bound.intersect(mPageBound);
487     }
488 
489     private boolean isDraggedIconInBound(DragEvent event) {
490         int iconLeft = (int) event.getX() - mDragCallback.mDragPoint.x;
491         int iconTop = (int) event.getY() - mDragCallback.mDragPoint.y;
492         return iconLeft >= 0 && iconTop >= 0 && (iconLeft + mIconScaledSize) < mAppItemWidth
493                 && (iconTop + mIconScaledSize) < mAppItemHeight;
494     }
495 
496     @AppItemBoundDirection
497     int getClosestBoundDirection(float eventX, float eventY) {
498         float cutoffThreshold = .25f;
499         if (isHorizontal(mPageOrientation)) {
500             float horizontalPosition = eventX / mAppItemWidth;
501             if (horizontalPosition < cutoffThreshold) {
502                 return AppItemBoundDirection.LEFT;
503             } else if (horizontalPosition > (1 - cutoffThreshold)) {
504                 return AppItemBoundDirection.RIGHT;
505             }
506             return AppItemBoundDirection.NONE;
507         }
508         float verticalPosition = eventY / mAppItemHeight;
509         if (verticalPosition < .5f) {
510             return AppItemBoundDirection.TOP;
511         } else if (verticalPosition > (1 - cutoffThreshold)) {
512             return AppItemBoundDirection.BOTTOM;
513         }
514         return AppItemBoundDirection.NONE;
515     }
516 
517     public boolean isMostRecentlySelected() {
518         return mComponentName != null
519                 && mComponentName.equals(mDragCallback.getPreviousSelectedComponent());
520     }
521 
522     /**
523      * A Callback contract between AppItemViewHolders and its listener. There are multiple view
524      * holders updating the callback but there should only be one listener.
525      *
526      * Drag drop operations will be started and listened to by each AppItemViewHolder, so all
527      * visual elements should be handled directly by the AppItemViewHolder. This class should only
528      * be used to communicate adapter data position changes.
529      */
530     public static class AppItemDragCallback {
531         private static final int NONE = -1;
532         private final AppItemDragListener mDragListener;
533         private final Handler mHandler;
534         private ComponentName mPreviousSelectedComponent;
535         private ComponentName mSelectedComponent;
536         private ComponentName mTargetedComponent;
537         private AppItemViewHolder mTargetedViewHolder;
538         private int mSelectedGridIndex = NONE;
539         private int mTargetedGridIndex = NONE;
540         // x y coordinate within the source app icon that the user finger is holding
541         private Point mDragPoint;
542         // x y coordinate within the viewHolder the drop event was registered
543         private Point mDropPoint;
544         // x y coordinate within the viewHolder which the drop animation should translate to
545         private Point mDropDestination;
546 
547         public AppItemDragCallback(AppItemDragListener listener) {
548             mDragListener = listener;
549             mHandler = new Handler(Looper.getMainLooper());
550         }
551 
552         /**
553          * The preparation step of drag drop process. Called when a long press gesture has been
554          * inputted or cancelled by the user.
555          */
556         public void notifyItemLongPressed(boolean isLongPressed) {
557             mDragListener.onItemLongPressed(isLongPressed);
558         }
559 
560         /**
561          * The initial step of the drag drop process. Called when the drag shadow of an app icon has
562          * been created, and should be immediately set as the drag source.
563          */
564         public void notifyItemSelected(AppItemViewHolder viewHolder, Point dragPoint) {
565             mDragPoint = new Point(dragPoint);
566             mDropDestination = new Point(viewHolder.mAppIconCenter);
567             mSelectedComponent = viewHolder.mComponentName;
568             mSelectedGridIndex = viewHolder.getAbsoluteAdapterPosition();
569             mDragListener.onItemSelected(mSelectedGridIndex);
570         }
571 
572         /**
573          * The second step of the drag drop process. Called when a drag shadow enters the bounds of
574          * a view holder (including the view holder containing the dragged icon itself).
575          */
576         public void notifyItemTargeted(@Nullable AppItemViewHolder viewHolder) {
577             if (mTargetedViewHolder != null && !mTargetedViewHolder.equals(viewHolder)) {
578                 mTargetedViewHolder.setStateTargeted(false);
579             }
580             if (viewHolder == null) {
581                 mTargetedComponent = null;
582                 mTargetedViewHolder = null;
583                 mTargetedGridIndex = NONE;
584                 return;
585             }
586             mTargetedComponent = viewHolder.mComponentName;
587             mTargetedViewHolder = viewHolder;
588             mTargetedGridIndex = viewHolder.getAbsoluteAdapterPosition();
589         }
590 
591         /**
592          * An intermediary step of the drag drop process. Called the drag shadow enters the
593          * view holder.
594          */
595         public void notifyItemDragged() {
596             mDragListener.onItemDragged();
597         }
598 
599         /**
600          * An intermediary step of the drag drop process. Called the drag shadow is dragged outside
601          * the view holder.
602          */
603         public void notifyDragExited(@NonNull AppItemViewHolder viewHolder,
604                 @AppItemBoundDirection int exitDirection) {
605             int gridPosition = viewHolder.getAbsoluteAdapterPosition();
606             mDragListener.onDragExited(gridPosition, exitDirection);
607         }
608 
609         /**
610          * The last step of drag and drop. Called when a ACTION_DROP event has been received by a
611          * view holder.
612          *
613          * Note that this event may never be called if the ACTION_DROP event was consumed by
614          * another onDragListener.
615          */
616         public void notifyItemDropped(Point dropPoint) {
617             mDropPoint = new Point(dropPoint);
618             if (mSelectedGridIndex != NONE && mTargetedGridIndex != NONE) {
619                 mDragListener.onItemDropped(mSelectedGridIndex, mTargetedGridIndex);
620                 resetCallbackState();
621             }
622         }
623 
624         /** Returns the previously selected component. */
625         public ComponentName getPreviousSelectedComponent() {
626             return mPreviousSelectedComponent;
627         }
628 
629         /** Reset component and callback state after a drag drop event has concluded */
630         public void resetCallbackState() {
631             if (mSelectedComponent != null) {
632                 mPreviousSelectedComponent = mSelectedComponent;
633             }
634             mSelectedComponent = mTargetedComponent = null;
635             mSelectedGridIndex = mTargetedGridIndex = NONE;
636         }
637 
638         /** Schedules a delayed task that enables drag and drop to start */
639         public void scheduleDragTask(Runnable runnable, long delay) {
640             mHandler.postDelayed(runnable, delay);
641         }
642 
643         /** Cancels all schedules tasks (i.e cancels intent to start drag drop) */
644         public void cancelDragTasks() {
645             mHandler.removeCallbacksAndMessages(null);
646         }
647     }
648 
649     /**
650      * Listener class that should be implemented by AppGridActivity.
651      */
652     public interface AppItemDragListener {
653         /** Listener method called during AppItemDragCallback.notifyLongPressed */
654         void onItemLongPressed(boolean longPressed);
655 
656         /** Listener method called during AppItemDragCallback.notifyItemSelected */
657         void onItemSelected(int gridPositionFrom);
658 
659         /** Listener method called during AppItemDragCallback.notifyDragEntered */
660         void onItemDragged();
661 
662         /** Listener method called during AppItemDragCallback.notifyDragExited */
663         void onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection);
664 
665         /** Listener method called during AppItemDragCallback.notifyItemDropped */
666         void onItemDropped(int gridPositionFrom, int gridPositionTo);
667     }
668 
669     private int getOpacity(boolean isLaunchable, boolean isDisabledByTos) {
670         if (isDisabledByTos) {
671             return R.dimen.app_icon_opacity_tos_disabled;
672         }
673         if (isLaunchable) {
674             return R.dimen.app_icon_opacity;
675         }
676         return R.dimen.app_icon_opacity_unavailable;
677     }
678 }
679