1 /*
2  * Copyright (C) 2008 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.launcher3;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.PropertyValuesHolder;
24 import android.annotation.SuppressLint;
25 import android.annotation.TargetApi;
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.graphics.Point;
29 import android.graphics.PointF;
30 import android.graphics.Rect;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.text.InputType;
34 import android.text.Selection;
35 import android.text.Spannable;
36 import android.util.AttributeSet;
37 import android.util.Log;
38 import android.view.ActionMode;
39 import android.view.KeyEvent;
40 import android.view.Menu;
41 import android.view.MenuItem;
42 import android.view.MotionEvent;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.view.accessibility.AccessibilityEvent;
46 import android.view.accessibility.AccessibilityManager;
47 import android.view.animation.AccelerateInterpolator;
48 import android.view.animation.AnimationUtils;
49 import android.view.inputmethod.EditorInfo;
50 import android.view.inputmethod.InputMethodManager;
51 import android.widget.LinearLayout;
52 import android.widget.TextView;
53 
54 import com.android.launcher3.CellLayout.CellInfo;
55 import com.android.launcher3.DragController.DragListener;
56 import com.android.launcher3.FolderInfo.FolderListener;
57 import com.android.launcher3.UninstallDropTarget.UninstallSource;
58 import com.android.launcher3.Workspace.ItemOperator;
59 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource;
60 import com.android.launcher3.config.FeatureFlags;
61 import com.android.launcher3.util.Thunk;
62 import com.android.launcher3.util.UiThreadCircularReveal;
63 
64 import java.util.ArrayList;
65 import java.util.Collections;
66 import java.util.Comparator;
67 
68 /**
69  * Represents a set of icons chosen by the user or generated by the system.
70  */
71 public class Folder extends LinearLayout implements DragSource, View.OnClickListener,
72         View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
73         View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource,
74         Stats.LaunchSourceProvider {
75     private static final String TAG = "Launcher.Folder";
76 
77     /**
78      * We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this
79      * results in CellLayout being measured as UNSPECIFIED, which it does not support.
80      */
81     private static final int MIN_CONTENT_DIMEN = 5;
82 
83     static final int STATE_NONE = -1;
84     static final int STATE_SMALL = 0;
85     static final int STATE_ANIMATING = 1;
86     static final int STATE_OPEN = 2;
87 
88     /**
89      * Time for which the scroll hint is shown before automatically changing page.
90      */
91     public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY;
92 
93     /**
94      * Fraction of icon width which behave as scroll region.
95      */
96     private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
97 
98     private static final int FOLDER_NAME_ANIMATION_DURATION = 633;
99 
100     private static final int REORDER_DELAY = 250;
101     private static final int ON_EXIT_CLOSE_DELAY = 400;
102     private static final Rect sTempRect = new Rect();
103 
104     private static String sDefaultFolderName;
105     private static String sHintText;
106 
107     private final Alarm mReorderAlarm = new Alarm();
108     private final Alarm mOnExitAlarm = new Alarm();
109     private final Alarm mOnScrollHintAlarm = new Alarm();
110     @Thunk final Alarm mScrollPauseAlarm = new Alarm();
111 
112     @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
113 
114     private final int mExpandDuration;
115     private final int mMaterialExpandDuration;
116     private final int mMaterialExpandStagger;
117 
118     private final InputMethodManager mInputMethodManager;
119 
120     protected final Launcher mLauncher;
121     protected DragController mDragController;
122     protected FolderInfo mInfo;
123 
124     @Thunk FolderIcon mFolderIcon;
125 
126     @Thunk FolderPagedView mContent;
127     @Thunk View mContentWrapper;
128     ExtendedEditText mFolderName;
129 
130     private View mFooter;
131     private int mFooterHeight;
132 
133     // Cell ranks used for drag and drop
134     @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank;
135 
136     @Thunk int mState = STATE_NONE;
137     private boolean mRearrangeOnClose = false;
138     boolean mItemsInvalidated = false;
139     private ShortcutInfo mCurrentDragInfo;
140     private View mCurrentDragView;
141     private boolean mIsExternalDrag;
142     boolean mSuppressOnAdd = false;
143     private boolean mDragInProgress = false;
144     private boolean mDeleteFolderOnDropCompleted = false;
145     private boolean mSuppressFolderDeletion = false;
146     private boolean mItemAddedBackToSelfViaIcon = false;
147     @Thunk float mFolderIconPivotX;
148     @Thunk float mFolderIconPivotY;
149     private boolean mIsEditingName = false;
150 
151     private boolean mDestroyed;
152 
153     @Thunk Runnable mDeferredAction;
154     private boolean mDeferDropAfterUninstall;
155     private boolean mUninstallSuccessful;
156 
157     // Folder scrolling
158     private int mScrollAreaOffset;
159 
160     @Thunk int mScrollHintDir = DragController.SCROLL_NONE;
161     @Thunk int mCurrentScrollDir = DragController.SCROLL_NONE;
162 
163     /**
164      * Used to inflate the Workspace from XML.
165      *
166      * @param context The application's context.
167      * @param attrs The attributes set containing the Workspace's customization values.
168      */
Folder(Context context, AttributeSet attrs)169     public Folder(Context context, AttributeSet attrs) {
170         super(context, attrs);
171         setAlwaysDrawnWithCacheEnabled(false);
172         mInputMethodManager = (InputMethodManager)
173                 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
174 
175         Resources res = getResources();
176         mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration);
177         mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration);
178         mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger);
179 
180         if (sDefaultFolderName == null) {
181             sDefaultFolderName = res.getString(R.string.folder_name);
182         }
183         if (sHintText == null) {
184             sHintText = res.getString(R.string.folder_hint_text);
185         }
186         mLauncher = (Launcher) context;
187         // We need this view to be focusable in touch mode so that when text editing of the folder
188         // name is complete, we have something to focus on, thus hiding the cursor and giving
189         // reliable behavior when clicking the text field (since it will always gain focus on click).
190         setFocusableInTouchMode(true);
191     }
192 
193     @Override
onFinishInflate()194     protected void onFinishInflate() {
195         super.onFinishInflate();
196         mContentWrapper = findViewById(R.id.folder_content_wrapper);
197         mContent = (FolderPagedView) findViewById(R.id.folder_content);
198         mContent.setFolder(this);
199 
200         mFolderName = (ExtendedEditText) findViewById(R.id.folder_name);
201         mFolderName.setOnBackKeyListener(new ExtendedEditText.OnBackKeyListener() {
202             @Override
203             public boolean onBackKey() {
204                 // Close the activity on back key press
205                 doneEditingFolderName(true);
206                 return false;
207             }
208         });
209         mFolderName.setOnFocusChangeListener(this);
210 
211         if (!Utilities.ATLEAST_MARSHMALLOW) {
212             // We disable action mode in older OSes where floating selection menu is not yet
213             // available.
214             mFolderName.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
215                 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
216                     return false;
217                 }
218 
219                 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
220                     return false;
221                 }
222 
223                 public void onDestroyActionMode(ActionMode mode) {
224                 }
225 
226                 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
227                     return false;
228                 }
229             });
230         }
231         mFolderName.setOnEditorActionListener(this);
232         mFolderName.setSelectAllOnFocus(true);
233         mFolderName.setInputType(mFolderName.getInputType() |
234                 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
235 
236         mFooter = findViewById(R.id.folder_footer);
237 
238         // We find out how tall footer wants to be (it is set to wrap_content), so that
239         // we can allocate the appropriate amount of space for it.
240         int measureSpec = MeasureSpec.UNSPECIFIED;
241         mFooter.measure(measureSpec, measureSpec);
242         mFooterHeight = mFooter.getMeasuredHeight();
243     }
244 
onClick(View v)245     public void onClick(View v) {
246         Object tag = v.getTag();
247         if (tag instanceof ShortcutInfo) {
248             mLauncher.onClick(v);
249         }
250     }
251 
onLongClick(View v)252     public boolean onLongClick(View v) {
253         // Return if global dragging is not enabled
254         if (!mLauncher.isDraggingEnabled()) return true;
255         return beginDrag(v, false);
256     }
257 
beginDrag(View v, boolean accessible)258     private boolean beginDrag(View v, boolean accessible) {
259         Object tag = v.getTag();
260         if (tag instanceof ShortcutInfo) {
261             ShortcutInfo item = (ShortcutInfo) tag;
262             if (!v.isInTouchMode()) {
263                 return false;
264             }
265 
266             mLauncher.getWorkspace().beginDragShared(v, new Point(), this, accessible);
267 
268             mCurrentDragInfo = item;
269             mEmptyCellRank = item.rank;
270             mCurrentDragView = v;
271 
272             mContent.removeItem(mCurrentDragView);
273             mInfo.remove(mCurrentDragInfo);
274             mDragInProgress = true;
275             mItemAddedBackToSelfViaIcon = false;
276         }
277         return true;
278     }
279 
280     @Override
startDrag(CellInfo cellInfo, boolean accessible)281     public void startDrag(CellInfo cellInfo, boolean accessible) {
282         beginDrag(cellInfo.cell, accessible);
283     }
284 
285     @Override
enableAccessibleDrag(boolean enable)286     public void enableAccessibleDrag(boolean enable) {
287         mLauncher.getSearchDropTargetBar().enableAccessibleDrag(enable);
288         for (int i = 0; i < mContent.getChildCount(); i++) {
289             mContent.getPageAt(i).enableAccessibleDrag(enable, CellLayout.FOLDER_ACCESSIBILITY_DRAG);
290         }
291 
292         mFooter.setImportantForAccessibility(enable ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS :
293             IMPORTANT_FOR_ACCESSIBILITY_AUTO);
294         mLauncher.getWorkspace().setAddNewPageOnDrag(!enable);
295     }
296 
isEditingName()297     public boolean isEditingName() {
298         return mIsEditingName;
299     }
300 
startEditingFolderName()301     public void startEditingFolderName() {
302         mFolderName.setHint("");
303         mIsEditingName = true;
304     }
305 
dismissEditingName()306     public void dismissEditingName() {
307         mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
308         doneEditingFolderName(true);
309     }
310 
doneEditingFolderName(boolean commit)311     public void doneEditingFolderName(boolean commit) {
312         mFolderName.setHint(sHintText);
313         // Convert to a string here to ensure that no other state associated with the text field
314         // gets saved.
315         String newTitle = mFolderName.getText().toString();
316         mInfo.setTitle(newTitle);
317         LauncherModel.updateItemInDatabase(mLauncher, mInfo);
318 
319         if (commit) {
320             sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
321                     String.format(getContext().getString(R.string.folder_renamed), newTitle));
322         }
323 
324         // This ensures that focus is gained every time the field is clicked, which selects all
325         // the text and brings up the soft keyboard if necessary.
326         mFolderName.clearFocus();
327 
328         Selection.setSelection((Spannable) mFolderName.getText(), 0, 0);
329         mIsEditingName = false;
330     }
331 
onEditorAction(TextView v, int actionId, KeyEvent event)332     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
333         if (actionId == EditorInfo.IME_ACTION_DONE) {
334             dismissEditingName();
335             return true;
336         }
337         return false;
338     }
339 
getEditTextRegion()340     public View getEditTextRegion() {
341         return mFolderName;
342     }
343 
344     /**
345      * We need to handle touch events to prevent them from falling through to the workspace below.
346      */
347     @SuppressLint("ClickableViewAccessibility")
348     @Override
onTouchEvent(MotionEvent ev)349     public boolean onTouchEvent(MotionEvent ev) {
350         return true;
351     }
352 
setDragController(DragController dragController)353     public void setDragController(DragController dragController) {
354         mDragController = dragController;
355     }
356 
setFolderIcon(FolderIcon icon)357     public void setFolderIcon(FolderIcon icon) {
358         mFolderIcon = icon;
359     }
360 
361     @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)362     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
363         // When the folder gets focus, we don't want to announce the list of items.
364         return true;
365     }
366 
367     /**
368      * @return the FolderInfo object associated with this folder
369      */
getInfo()370     public FolderInfo getInfo() {
371         return mInfo;
372     }
373 
bind(FolderInfo info)374     void bind(FolderInfo info) {
375         mInfo = info;
376         ArrayList<ShortcutInfo> children = info.contents;
377         Collections.sort(children, ITEM_POS_COMPARATOR);
378 
379         ArrayList<ShortcutInfo> overflow = mContent.bindItems(children);
380 
381         // If our folder has too many items we prune them from the list. This is an issue
382         // when upgrading from the old Folders implementation which could contain an unlimited
383         // number of items.
384         for (ShortcutInfo item: overflow) {
385             mInfo.remove(item);
386             LauncherModel.deleteItemFromDatabase(mLauncher, item);
387         }
388 
389         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
390         if (lp == null) {
391             lp = new DragLayer.LayoutParams(0, 0);
392             lp.customPosition = true;
393             setLayoutParams(lp);
394         }
395         centerAboutIcon();
396 
397         mItemsInvalidated = true;
398         updateTextViewFocus();
399         mInfo.addListener(this);
400 
401         if (!sDefaultFolderName.contentEquals(mInfo.title)) {
402             mFolderName.setText(mInfo.title);
403         } else {
404             mFolderName.setText("");
405         }
406 
407         // In case any children didn't come across during loading, clean up the folder accordingly
408         mFolderIcon.post(new Runnable() {
409             public void run() {
410                 if (getItemCount() <= 1) {
411                     replaceFolderWithFinalItem();
412                 }
413             }
414         });
415     }
416 
417     /**
418      * Creates a new UserFolder, inflated from R.layout.user_folder.
419      *
420      * @param launcher The main activity.
421      *
422      * @return A new UserFolder.
423      */
424     @SuppressLint("InflateParams")
fromXml(Launcher launcher)425     static Folder fromXml(Launcher launcher) {
426         return (Folder) launcher.getLayoutInflater().inflate(
427                 FeatureFlags.LAUNCHER3_ICON_NORMALIZATION
428                         ? R.layout.user_folder_icon_normalized : R.layout.user_folder, null);
429     }
430 
431     /**
432      * This method is intended to make the UserFolder to be visually identical in size and position
433      * to its associated FolderIcon. This allows for a seamless transition into the expanded state.
434      */
positionAndSizeAsIcon()435     private void positionAndSizeAsIcon() {
436         if (!(getParent() instanceof DragLayer)) return;
437         setScaleX(0.8f);
438         setScaleY(0.8f);
439         setAlpha(0f);
440         mState = STATE_SMALL;
441     }
442 
prepareReveal()443     private void prepareReveal() {
444         setScaleX(1f);
445         setScaleY(1f);
446         setAlpha(1f);
447         mState = STATE_SMALL;
448     }
449 
animateOpen()450     public void animateOpen() {
451         if (!(getParent() instanceof DragLayer)) return;
452 
453         mContent.completePendingPageChanges();
454         if (!mDragInProgress) {
455             // Open on the first page.
456             mContent.snapToPageImmediately(0);
457         }
458 
459         // This is set to true in close(), but isn't reset to false until onDropCompleted(). This
460         // leads to an consistent state if you drag out of the folder and drag back in without
461         // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
462         mDeleteFolderOnDropCompleted = false;
463 
464         Animator openFolderAnim = null;
465         final Runnable onCompleteRunnable;
466         if (!Utilities.ATLEAST_LOLLIPOP) {
467             positionAndSizeAsIcon();
468             centerAboutIcon();
469 
470             PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1);
471             PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f);
472             PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f);
473             final ObjectAnimator oa =
474                 LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
475             oa.setDuration(mExpandDuration);
476             openFolderAnim = oa;
477 
478             setLayerType(LAYER_TYPE_HARDWARE, null);
479             onCompleteRunnable = new Runnable() {
480                 @Override
481                 public void run() {
482                     setLayerType(LAYER_TYPE_NONE, null);
483                 }
484             };
485         } else {
486             prepareReveal();
487             centerAboutIcon();
488 
489             AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
490             int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
491             int height = getFolderHeight();
492 
493             float transX = - 0.075f * (width / 2 - getPivotX());
494             float transY = - 0.075f * (height / 2 - getPivotY());
495             setTranslationX(transX);
496             setTranslationY(transY);
497             PropertyValuesHolder tx = PropertyValuesHolder.ofFloat("translationX", transX, 0);
498             PropertyValuesHolder ty = PropertyValuesHolder.ofFloat("translationY", transY, 0);
499 
500             Animator drift = ObjectAnimator.ofPropertyValuesHolder(this, tx, ty);
501             drift.setDuration(mMaterialExpandDuration);
502             drift.setStartDelay(mMaterialExpandStagger);
503             drift.setInterpolator(new LogDecelerateInterpolator(100, 0));
504 
505             int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX());
506             int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY());
507             float radius = (float) Math.hypot(rx, ry);
508 
509             Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(),
510                     (int) getPivotY(), 0, radius);
511             reveal.setDuration(mMaterialExpandDuration);
512             reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
513 
514             mContentWrapper.setAlpha(0f);
515             Animator iconsAlpha = ObjectAnimator.ofFloat(mContentWrapper, "alpha", 0f, 1f);
516             iconsAlpha.setDuration(mMaterialExpandDuration);
517             iconsAlpha.setStartDelay(mMaterialExpandStagger);
518             iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
519 
520             mFooter.setAlpha(0f);
521             Animator textAlpha = ObjectAnimator.ofFloat(mFooter, "alpha", 0f, 1f);
522             textAlpha.setDuration(mMaterialExpandDuration);
523             textAlpha.setStartDelay(mMaterialExpandStagger);
524             textAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
525 
526             anim.play(drift);
527             anim.play(iconsAlpha);
528             anim.play(textAlpha);
529             anim.play(reveal);
530 
531             openFolderAnim = anim;
532 
533             mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null);
534             mFooter.setLayerType(LAYER_TYPE_HARDWARE, null);
535             onCompleteRunnable = new Runnable() {
536                 @Override
537                 public void run() {
538                     mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
539                     mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
540                 }
541             };
542         }
543         openFolderAnim.addListener(new AnimatorListenerAdapter() {
544             @Override
545             public void onAnimationStart(Animator animation) {
546                 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
547                         mContent.getAccessibilityDescription());
548                 mState = STATE_ANIMATING;
549             }
550             @Override
551             public void onAnimationEnd(Animator animation) {
552                 mState = STATE_OPEN;
553 
554                 onCompleteRunnable.run();
555                 mContent.setFocusOnFirstChild();
556             }
557         });
558 
559         // Footer animation
560         if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) {
561             int footerWidth = mContent.getDesiredWidth()
562                     - mFooter.getPaddingLeft() - mFooter.getPaddingRight();
563 
564             float textWidth =  mFolderName.getPaint().measureText(mFolderName.getText().toString());
565             float translation = (footerWidth - textWidth) / 2;
566             mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation);
567             mContent.setMarkerScale(0);
568 
569             // Do not update the flag if we are in drag mode. The flag will be updated, when we
570             // actually drop the icon.
571             final boolean updateAnimationFlag = !mDragInProgress;
572             openFolderAnim.addListener(new AnimatorListenerAdapter() {
573 
574                 @Override
575                 public void onAnimationEnd(Animator animation) {
576                     mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION)
577                         .translationX(0)
578                         .setInterpolator(Utilities.ATLEAST_LOLLIPOP ?
579                                 AnimationUtils.loadInterpolator(mLauncher,
580                                         android.R.interpolator.fast_out_slow_in)
581                                 : new LogDecelerateInterpolator(100, 0));
582                     mContent.animateMarkers();
583 
584                     if (updateAnimationFlag) {
585                         mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
586                     }
587                 }
588             });
589         } else {
590             mFolderName.setTranslationX(0);
591             mContent.setMarkerScale(1);
592         }
593 
594         openFolderAnim.start();
595 
596         // Make sure the folder picks up the last drag move even if the finger doesn't move.
597         if (mDragController.isDragging()) {
598             mDragController.forceTouchMove();
599         }
600 
601         FolderPagedView pages = (FolderPagedView) mContent;
602         pages.verifyVisibleHighResIcons(pages.getNextPage());
603     }
604 
beginExternalDrag(ShortcutInfo item)605     public void beginExternalDrag(ShortcutInfo item) {
606         mCurrentDragInfo = item;
607         mEmptyCellRank = mContent.allocateRankForNewItem(item);
608         mIsExternalDrag = true;
609         mDragInProgress = true;
610 
611         // Since this folder opened by another controller, it might not get onDrop or
612         // onDropComplete. Perform cleanup once drag-n-drop ends.
613         mDragController.addDragListener(this);
614     }
615 
616     @Override
onDragStart(DragSource source, Object info, int dragAction)617     public void onDragStart(DragSource source, Object info, int dragAction) { }
618 
619     @Override
onDragEnd()620     public void onDragEnd() {
621         if (mIsExternalDrag && mDragInProgress) {
622             completeDragExit();
623         }
624         mDragController.removeDragListener(this);
625     }
626 
sendCustomAccessibilityEvent(int type, String text)627     @Thunk void sendCustomAccessibilityEvent(int type, String text) {
628         AccessibilityManager accessibilityManager = (AccessibilityManager)
629                 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
630         if (accessibilityManager.isEnabled()) {
631             AccessibilityEvent event = AccessibilityEvent.obtain(type);
632             onInitializeAccessibilityEvent(event);
633             event.getText().add(text);
634             accessibilityManager.sendAccessibilityEvent(event);
635         }
636     }
637 
animateClosed()638     public void animateClosed() {
639         if (!(getParent() instanceof DragLayer)) return;
640         PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0);
641         PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f);
642         PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f);
643         final ObjectAnimator oa =
644                 LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
645 
646         oa.addListener(new AnimatorListenerAdapter() {
647             @Override
648             public void onAnimationEnd(Animator animation) {
649                 setLayerType(LAYER_TYPE_NONE, null);
650                 close(true);
651             }
652             @Override
653             public void onAnimationStart(Animator animation) {
654                 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
655                         getContext().getString(R.string.folder_closed));
656                 mState = STATE_ANIMATING;
657             }
658         });
659         oa.setDuration(mExpandDuration);
660         setLayerType(LAYER_TYPE_HARDWARE, null);
661         oa.start();
662     }
663 
close(boolean wasAnimated)664     public void close(boolean wasAnimated) {
665         // TODO: Clear all active animations.
666         DragLayer parent = (DragLayer) getParent();
667         if (parent != null) {
668             parent.removeView(this);
669         }
670         mDragController.removeDropTarget(this);
671         clearFocus();
672         if (wasAnimated) {
673             mFolderIcon.requestFocus();
674         }
675 
676         if (mRearrangeOnClose) {
677             rearrangeChildren();
678             mRearrangeOnClose = false;
679         }
680         if (getItemCount() <= 1) {
681             if (!mDragInProgress && !mSuppressFolderDeletion) {
682                 replaceFolderWithFinalItem();
683             } else if (mDragInProgress) {
684                 mDeleteFolderOnDropCompleted = true;
685             }
686         }
687         mSuppressFolderDeletion = false;
688         clearDragInfo();
689         mState = STATE_SMALL;
690     }
691 
acceptDrop(DragObject d)692     public boolean acceptDrop(DragObject d) {
693         final ItemInfo item = (ItemInfo) d.dragInfo;
694         final int itemType = item.itemType;
695         return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
696                     itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
697                     !isFull());
698     }
699 
onDragEnter(DragObject d)700     public void onDragEnter(DragObject d) {
701         mPrevTargetRank = -1;
702         mOnExitAlarm.cancelAlarm();
703         // Get the area offset such that the folder only closes if half the drag icon width
704         // is outside the folder area
705         mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset;
706     }
707 
708     OnAlarmListener mReorderAlarmListener = new OnAlarmListener() {
709         public void onAlarm(Alarm alarm) {
710             mContent.realTimeReorder(mEmptyCellRank, mTargetRank);
711             mEmptyCellRank = mTargetRank;
712         }
713     };
714 
715     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
isLayoutRtl()716     public boolean isLayoutRtl() {
717         return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
718     }
719 
720     @Override
onDragOver(DragObject d)721     public void onDragOver(DragObject d) {
722         onDragOver(d, REORDER_DELAY);
723     }
724 
getTargetRank(DragObject d, float[] recycle)725     private int getTargetRank(DragObject d, float[] recycle) {
726         recycle = d.getVisualCenter(recycle);
727         return mContent.findNearestArea(
728                 (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop());
729     }
730 
onDragOver(DragObject d, int reorderDelay)731     @Thunk void onDragOver(DragObject d, int reorderDelay) {
732         if (mScrollPauseAlarm.alarmPending()) {
733             return;
734         }
735         final float[] r = new float[2];
736         mTargetRank = getTargetRank(d, r);
737 
738         if (mTargetRank != mPrevTargetRank) {
739             mReorderAlarm.cancelAlarm();
740             mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
741             mReorderAlarm.setAlarm(REORDER_DELAY);
742             mPrevTargetRank = mTargetRank;
743 
744             if (d.stateAnnouncer != null) {
745                 d.stateAnnouncer.announce(getContext().getString(R.string.move_to_position,
746                         mTargetRank + 1));
747             }
748         }
749 
750         float x = r[0];
751         int currentPage = mContent.getNextPage();
752 
753         float cellOverlap = mContent.getCurrentCellLayout().getCellWidth()
754                 * ICON_OVERSCROLL_WIDTH_FACTOR;
755         boolean isOutsideLeftEdge = x < cellOverlap;
756         boolean isOutsideRightEdge = x > (getWidth() - cellOverlap);
757 
758         if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) {
759             showScrollHint(DragController.SCROLL_LEFT, d);
760         } else if (currentPage < (mContent.getPageCount() - 1)
761                 && (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) {
762             showScrollHint(DragController.SCROLL_RIGHT, d);
763         } else {
764             mOnScrollHintAlarm.cancelAlarm();
765             if (mScrollHintDir != DragController.SCROLL_NONE) {
766                 mContent.clearScrollHint();
767                 mScrollHintDir = DragController.SCROLL_NONE;
768             }
769         }
770     }
771 
showScrollHint(int direction, DragObject d)772     private void showScrollHint(int direction, DragObject d) {
773         // Show scroll hint on the right
774         if (mScrollHintDir != direction) {
775             mContent.showScrollHint(direction);
776             mScrollHintDir = direction;
777         }
778 
779         // Set alarm for when the hint is complete
780         if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) {
781             mCurrentScrollDir = direction;
782             mOnScrollHintAlarm.cancelAlarm();
783             mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d));
784             mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION);
785 
786             mReorderAlarm.cancelAlarm();
787             mTargetRank = mEmptyCellRank;
788         }
789     }
790 
791     OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
792         public void onAlarm(Alarm alarm) {
793             completeDragExit();
794         }
795     };
796 
completeDragExit()797     public void completeDragExit() {
798         if (mInfo.opened) {
799             mLauncher.closeFolder();
800             mRearrangeOnClose = true;
801         } else if (mState == STATE_ANIMATING) {
802             mRearrangeOnClose = true;
803         } else {
804             rearrangeChildren();
805             clearDragInfo();
806         }
807     }
808 
clearDragInfo()809     private void clearDragInfo() {
810         mCurrentDragInfo = null;
811         mCurrentDragView = null;
812         mSuppressOnAdd = false;
813         mIsExternalDrag = false;
814     }
815 
onDragExit(DragObject d)816     public void onDragExit(DragObject d) {
817         // We only close the folder if this is a true drag exit, ie. not because
818         // a drop has occurred above the folder.
819         if (!d.dragComplete) {
820             mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener);
821             mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
822         }
823         mReorderAlarm.cancelAlarm();
824 
825         mOnScrollHintAlarm.cancelAlarm();
826         mScrollPauseAlarm.cancelAlarm();
827         if (mScrollHintDir != DragController.SCROLL_NONE) {
828             mContent.clearScrollHint();
829             mScrollHintDir = DragController.SCROLL_NONE;
830         }
831     }
832 
833     /**
834      * When performing an accessibility drop, onDrop is sent immediately after onDragEnter. So we
835      * need to complete all transient states based on timers.
836      */
837     @Override
prepareAccessibilityDrop()838     public void prepareAccessibilityDrop() {
839         if (mReorderAlarm.alarmPending()) {
840             mReorderAlarm.cancelAlarm();
841             mReorderAlarmListener.onAlarm(mReorderAlarm);
842         }
843     }
844 
onDropCompleted(final View target, final DragObject d, final boolean isFlingToDelete, final boolean success)845     public void onDropCompleted(final View target, final DragObject d,
846             final boolean isFlingToDelete, final boolean success) {
847         if (mDeferDropAfterUninstall) {
848             Log.d(TAG, "Deferred handling drop because waiting for uninstall.");
849             mDeferredAction = new Runnable() {
850                     public void run() {
851                         onDropCompleted(target, d, isFlingToDelete, success);
852                         mDeferredAction = null;
853                     }
854                 };
855             return;
856         }
857 
858         boolean beingCalledAfterUninstall = mDeferredAction != null;
859         boolean successfulDrop =
860                 success && (!beingCalledAfterUninstall || mUninstallSuccessful);
861 
862         if (successfulDrop) {
863             if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
864                 replaceFolderWithFinalItem();
865             }
866         } else {
867             // The drag failed, we need to return the item to the folder
868             ShortcutInfo info = (ShortcutInfo) d.dragInfo;
869             View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info)
870                     ? mCurrentDragView : mContent.createNewView(info);
871             ArrayList<View> views = getItemsInReadingOrder();
872             views.add(info.rank, icon);
873             mContent.arrangeChildren(views, views.size());
874             mItemsInvalidated = true;
875 
876             mSuppressOnAdd = true;
877             mFolderIcon.onDrop(d);
878             mSuppressOnAdd = false;
879         }
880 
881         if (target != this) {
882             if (mOnExitAlarm.alarmPending()) {
883                 mOnExitAlarm.cancelAlarm();
884                 if (!successfulDrop) {
885                     mSuppressFolderDeletion = true;
886                 }
887                 mScrollPauseAlarm.cancelAlarm();
888                 completeDragExit();
889             }
890         }
891 
892         mDeleteFolderOnDropCompleted = false;
893         mDragInProgress = false;
894         mItemAddedBackToSelfViaIcon = false;
895         mCurrentDragInfo = null;
896         mCurrentDragView = null;
897         mSuppressOnAdd = false;
898 
899         // Reordering may have occured, and we need to save the new item locations. We do this once
900         // at the end to prevent unnecessary database operations.
901         updateItemLocationsInDatabaseBatch();
902 
903         // Use the item count to check for multi-page as the folder UI may not have
904         // been refreshed yet.
905         if (getItemCount() <= mContent.itemsPerPage()) {
906             // Show the animation, next time something is added to the folder.
907             mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher);
908         }
909     }
910 
911     @Override
deferCompleteDropAfterUninstallActivity()912     public void deferCompleteDropAfterUninstallActivity() {
913         mDeferDropAfterUninstall = true;
914     }
915 
916     @Override
onUninstallActivityReturned(boolean success)917     public void onUninstallActivityReturned(boolean success) {
918         mDeferDropAfterUninstall = false;
919         mUninstallSuccessful = success;
920         if (mDeferredAction != null) {
921             mDeferredAction.run();
922         }
923     }
924 
925     @Override
getIntrinsicIconScaleFactor()926     public float getIntrinsicIconScaleFactor() {
927         return 1f;
928     }
929 
930     @Override
supportsFlingToDelete()931     public boolean supportsFlingToDelete() {
932         return true;
933     }
934 
935     @Override
supportsAppInfoDropTarget()936     public boolean supportsAppInfoDropTarget() {
937         return false;
938     }
939 
940     @Override
supportsDeleteDropTarget()941     public boolean supportsDeleteDropTarget() {
942         return true;
943     }
944 
945     @Override
onFlingToDelete(DragObject d, PointF vec)946     public void onFlingToDelete(DragObject d, PointF vec) {
947         // Do nothing
948     }
949 
950     @Override
onFlingToDeleteCompleted()951     public void onFlingToDeleteCompleted() {
952         // Do nothing
953     }
954 
updateItemLocationsInDatabaseBatch()955     private void updateItemLocationsInDatabaseBatch() {
956         ArrayList<View> list = getItemsInReadingOrder();
957         ArrayList<ItemInfo> items = new ArrayList<ItemInfo>();
958         for (int i = 0; i < list.size(); i++) {
959             View v = list.get(i);
960             ItemInfo info = (ItemInfo) v.getTag();
961             info.rank = i;
962             items.add(info);
963         }
964 
965         LauncherModel.moveItemsInDatabase(mLauncher, items, mInfo.id, 0);
966     }
967 
addItemLocationsInDatabase()968     public void addItemLocationsInDatabase() {
969         ArrayList<View> list = getItemsInReadingOrder();
970         for (int i = 0; i < list.size(); i++) {
971             View v = list.get(i);
972             ItemInfo info = (ItemInfo) v.getTag();
973             LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0,
974                     info.cellX, info.cellY);
975         }
976     }
977 
notifyDrop()978     public void notifyDrop() {
979         if (mDragInProgress) {
980             mItemAddedBackToSelfViaIcon = true;
981         }
982     }
983 
isDropEnabled()984     public boolean isDropEnabled() {
985         return true;
986     }
987 
isFull()988     public boolean isFull() {
989         return mContent.isFull();
990     }
991 
centerAboutIcon()992     private void centerAboutIcon() {
993         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
994 
995         DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer);
996         int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
997         int height = getFolderHeight();
998 
999         float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect);
1000 
1001         DeviceProfile grid = mLauncher.getDeviceProfile();
1002 
1003         int centerX = (int) (sTempRect.left + sTempRect.width() * scale / 2);
1004         int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2);
1005         int centeredLeft = centerX - width / 2;
1006         int centeredTop = centerY - height / 2;
1007 
1008         // We need to bound the folder to the currently visible workspace area
1009         mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect);
1010         int left = Math.min(Math.max(sTempRect.left, centeredLeft),
1011                 sTempRect.left + sTempRect.width() - width);
1012         int top = Math.min(Math.max(sTempRect.top, centeredTop),
1013                 sTempRect.top + sTempRect.height() - height);
1014         if (grid.isPhone && (grid.availableWidthPx - width) < grid.iconSizePx) {
1015             // Center the folder if it is full (on phones only)
1016             left = (grid.availableWidthPx - width) / 2;
1017         } else if (width >= sTempRect.width()) {
1018             // If the folder doesn't fit within the bounds, center it about the desired bounds
1019             left = sTempRect.left + (sTempRect.width() - width) / 2;
1020         }
1021         if (height >= sTempRect.height()) {
1022             top = sTempRect.top + (sTempRect.height() - height) / 2;
1023         }
1024 
1025         int folderPivotX = width / 2 + (centeredLeft - left);
1026         int folderPivotY = height / 2 + (centeredTop - top);
1027         setPivotX(folderPivotX);
1028         setPivotY(folderPivotY);
1029         mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() *
1030                 (1.0f * folderPivotX / width));
1031         mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() *
1032                 (1.0f * folderPivotY / height));
1033 
1034         lp.width = width;
1035         lp.height = height;
1036         lp.x = left;
1037         lp.y = top;
1038     }
1039 
getPivotXForIconAnimation()1040     float getPivotXForIconAnimation() {
1041         return mFolderIconPivotX;
1042     }
getPivotYForIconAnimation()1043     float getPivotYForIconAnimation() {
1044         return mFolderIconPivotY;
1045     }
1046 
getContentAreaHeight()1047     private int getContentAreaHeight() {
1048         DeviceProfile grid = mLauncher.getDeviceProfile();
1049         Rect workspacePadding = grid.getWorkspacePadding(mContent.mIsRtl);
1050         int maxContentAreaHeight = grid.availableHeightPx -
1051                 workspacePadding.top - workspacePadding.bottom -
1052                 mFooterHeight;
1053         int height = Math.min(maxContentAreaHeight,
1054                 mContent.getDesiredHeight());
1055         return Math.max(height, MIN_CONTENT_DIMEN);
1056     }
1057 
getContentAreaWidth()1058     private int getContentAreaWidth() {
1059         return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
1060     }
1061 
getFolderHeight()1062     private int getFolderHeight() {
1063         return getFolderHeight(getContentAreaHeight());
1064     }
1065 
getFolderHeight(int contentAreaHeight)1066     private int getFolderHeight(int contentAreaHeight) {
1067         return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
1068     }
1069 
onMeasure(int widthMeasureSpec, int heightMeasureSpec)1070     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1071         int contentWidth = getContentAreaWidth();
1072         int contentHeight = getContentAreaHeight();
1073 
1074         int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY);
1075         int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY);
1076 
1077         mContent.setFixedSize(contentWidth, contentHeight);
1078         mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec);
1079 
1080         if (mContent.getChildCount() > 0) {
1081             int cellIconGap = (mContent.getPageAt(0).getCellWidth()
1082                     - mLauncher.getDeviceProfile().iconSizePx) / 2;
1083             mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap,
1084                     mFooter.getPaddingTop(),
1085                     mContent.getPaddingRight() + cellIconGap,
1086                     mFooter.getPaddingBottom());
1087         }
1088         mFooter.measure(contentAreaWidthSpec,
1089                 MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY));
1090 
1091         int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
1092         int folderHeight = getFolderHeight(contentHeight);
1093         setMeasuredDimension(folderWidth, folderHeight);
1094     }
1095 
1096     /**
1097      * Rearranges the children based on their rank.
1098      */
rearrangeChildren()1099     public void rearrangeChildren() {
1100         rearrangeChildren(-1);
1101     }
1102 
1103     /**
1104      * Rearranges the children based on their rank.
1105      * @param itemCount if greater than the total children count, empty spaces are left at the end,
1106      * otherwise it is ignored.
1107      */
rearrangeChildren(int itemCount)1108     public void rearrangeChildren(int itemCount) {
1109         ArrayList<View> views = getItemsInReadingOrder();
1110         mContent.arrangeChildren(views, Math.max(itemCount, views.size()));
1111         mItemsInvalidated = true;
1112     }
1113 
1114     // TODO remove this once GSA code fix is submitted
getContent()1115     public ViewGroup getContent() {
1116         return (ViewGroup) mContent;
1117     }
1118 
getItemCount()1119     public int getItemCount() {
1120         return mContent.getItemCount();
1121     }
1122 
replaceFolderWithFinalItem()1123     @Thunk void replaceFolderWithFinalItem() {
1124         // Add the last remaining child to the workspace in place of the folder
1125         Runnable onCompleteRunnable = new Runnable() {
1126             @Override
1127             public void run() {
1128                 int itemCount = mInfo.contents.size();
1129                 if (itemCount <= 1) {
1130                     View newIcon = null;
1131 
1132                     if (itemCount == 1) {
1133                         // Move the item from the folder to the workspace, in the position of the
1134                         // folder
1135                         CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container,
1136                                 mInfo.screenId);
1137                         ShortcutInfo finalItem = mInfo.contents.remove(0);
1138                         newIcon = mLauncher.createShortcut(cellLayout, finalItem);
1139                         LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
1140                                 mInfo.screenId, mInfo.cellX, mInfo.cellY);
1141                     }
1142 
1143                     // Remove the folder
1144                     mLauncher.removeItem(mFolderIcon, mInfo, true /* deleteFromDb */);
1145                     if (mFolderIcon instanceof DropTarget) {
1146                         mDragController.removeDropTarget((DropTarget) mFolderIcon);
1147                     }
1148 
1149                     if (newIcon != null) {
1150                         // We add the child after removing the folder to prevent both from existing
1151                         // at the same time in the CellLayout.  We need to add the new item with
1152                         // addInScreenFromBind() to ensure that hotseat items are placed correctly.
1153                         mLauncher.getWorkspace().addInScreenFromBind(newIcon, mInfo.container,
1154                                 mInfo.screenId, mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY);
1155 
1156                         // Focus the newly created child
1157                         newIcon.requestFocus();
1158                     }
1159                 }
1160             }
1161         };
1162         View finalChild = mContent.getLastItem();
1163         if (finalChild != null) {
1164             mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable);
1165         } else {
1166             onCompleteRunnable.run();
1167         }
1168         mDestroyed = true;
1169     }
1170 
isDestroyed()1171     boolean isDestroyed() {
1172         return mDestroyed;
1173     }
1174 
1175     // This method keeps track of the first and last item in the folder for the purposes
1176     // of keyboard focus
updateTextViewFocus()1177     public void updateTextViewFocus() {
1178         final View firstChild = mContent.getFirstItem();
1179         final View lastChild = mContent.getLastItem();
1180         if (firstChild != null && lastChild != null) {
1181             mFolderName.setNextFocusDownId(lastChild.getId());
1182             mFolderName.setNextFocusRightId(lastChild.getId());
1183             mFolderName.setNextFocusLeftId(lastChild.getId());
1184             mFolderName.setNextFocusUpId(lastChild.getId());
1185             // Hitting TAB from the folder name wraps around to the first item on the current
1186             // folder page, and hitting SHIFT+TAB from that item wraps back to the folder name.
1187             mFolderName.setNextFocusForwardId(firstChild.getId());
1188             // When clicking off the folder when editing the name, this Folder gains focus. When
1189             // pressing an arrow key from that state, give the focus to the first item.
1190             this.setNextFocusDownId(firstChild.getId());
1191             this.setNextFocusRightId(firstChild.getId());
1192             this.setNextFocusLeftId(firstChild.getId());
1193             this.setNextFocusUpId(firstChild.getId());
1194             // When pressing shift+tab in the above state, give the focus to the last item.
1195             setOnKeyListener(new OnKeyListener() {
1196                 @Override
1197                 public boolean onKey(View v, int keyCode, KeyEvent event) {
1198                     boolean isShiftPlusTab = keyCode == KeyEvent.KEYCODE_TAB &&
1199                             event.hasModifiers(KeyEvent.META_SHIFT_ON);
1200                     if (isShiftPlusTab && Folder.this.isFocused()) {
1201                         return lastChild.requestFocus();
1202                     }
1203                     return false;
1204                 }
1205             });
1206         }
1207     }
1208 
onDrop(DragObject d)1209     public void onDrop(DragObject d) {
1210         Runnable cleanUpRunnable = null;
1211 
1212         // If we are coming from All Apps space, we defer removing the extra empty screen
1213         // until the folder closes
1214         if (d.dragSource != mLauncher.getWorkspace() && !(d.dragSource instanceof Folder)) {
1215             cleanUpRunnable = new Runnable() {
1216                 @Override
1217                 public void run() {
1218                     mLauncher.exitSpringLoadedDragModeDelayed(true,
1219                             Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT,
1220                             null);
1221                 }
1222             };
1223         }
1224 
1225         // If the icon was dropped while the page was being scrolled, we need to compute
1226         // the target location again such that the icon is placed of the final page.
1227         if (!mContent.rankOnCurrentPage(mEmptyCellRank)) {
1228             // Reorder again.
1229             mTargetRank = getTargetRank(d, null);
1230 
1231             // Rearrange items immediately.
1232             mReorderAlarmListener.onAlarm(mReorderAlarm);
1233 
1234             mOnScrollHintAlarm.cancelAlarm();
1235             mScrollPauseAlarm.cancelAlarm();
1236         }
1237         mContent.completePendingPageChanges();
1238 
1239         View currentDragView;
1240         ShortcutInfo si = mCurrentDragInfo;
1241         if (mIsExternalDrag) {
1242             currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank);
1243             // Actually move the item in the database if it was an external drag. Call this
1244             // before creating the view, so that ShortcutInfo is updated appropriately.
1245             LauncherModel.addOrMoveItemInDatabase(
1246                     mLauncher, si, mInfo.id, 0, si.cellX, si.cellY);
1247 
1248             // We only need to update the locations if it doesn't get handled in #onDropCompleted.
1249             if (d.dragSource != this) {
1250                 updateItemLocationsInDatabaseBatch();
1251             }
1252             mIsExternalDrag = false;
1253         } else {
1254             currentDragView = mCurrentDragView;
1255             mContent.addViewForRank(currentDragView, si, mEmptyCellRank);
1256         }
1257 
1258         if (d.dragView.hasDrawn()) {
1259 
1260             // Temporarily reset the scale such that the animation target gets calculated correctly.
1261             float scaleX = getScaleX();
1262             float scaleY = getScaleY();
1263             setScaleX(1.0f);
1264             setScaleY(1.0f);
1265             mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, currentDragView,
1266                     cleanUpRunnable, null);
1267             setScaleX(scaleX);
1268             setScaleY(scaleY);
1269         } else {
1270             d.deferDragViewCleanupPostAnimation = false;
1271             currentDragView.setVisibility(VISIBLE);
1272         }
1273         mItemsInvalidated = true;
1274         rearrangeChildren();
1275 
1276         // Temporarily suppress the listener, as we did all the work already here.
1277         mSuppressOnAdd = true;
1278         mInfo.add(si);
1279         mSuppressOnAdd = false;
1280         // Clear the drag info, as it is no longer being dragged.
1281         mCurrentDragInfo = null;
1282         mDragInProgress = false;
1283 
1284         if (mContent.getPageCount() > 1) {
1285             // The animation has already been shown while opening the folder.
1286             mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
1287         }
1288     }
1289 
1290     // This is used so the item doesn't immediately appear in the folder when added. In one case
1291     // we need to create the illusion that the item isn't added back to the folder yet, to
1292     // to correspond to the animation of the icon back into the folder. This is
hideItem(ShortcutInfo info)1293     public void hideItem(ShortcutInfo info) {
1294         View v = getViewForInfo(info);
1295         v.setVisibility(INVISIBLE);
1296     }
showItem(ShortcutInfo info)1297     public void showItem(ShortcutInfo info) {
1298         View v = getViewForInfo(info);
1299         v.setVisibility(VISIBLE);
1300     }
1301 
1302     @Override
onAdd(ShortcutInfo item)1303     public void onAdd(ShortcutInfo item) {
1304         // If the item was dropped onto this open folder, we have done the work associated
1305         // with adding the item to the folder, as indicated by mSuppressOnAdd being set
1306         if (mSuppressOnAdd) return;
1307         mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item));
1308         mItemsInvalidated = true;
1309         LauncherModel.addOrMoveItemInDatabase(
1310                 mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
1311     }
1312 
onRemove(ShortcutInfo item)1313     public void onRemove(ShortcutInfo item) {
1314         mItemsInvalidated = true;
1315         // If this item is being dragged from this open folder, we have already handled
1316         // the work associated with removing the item, so we don't have to do anything here.
1317         if (item == mCurrentDragInfo) return;
1318         View v = getViewForInfo(item);
1319         mContent.removeItem(v);
1320         if (mState == STATE_ANIMATING) {
1321             mRearrangeOnClose = true;
1322         } else {
1323             rearrangeChildren();
1324         }
1325         if (getItemCount() <= 1) {
1326             if (mInfo.opened) {
1327                 mLauncher.closeFolder(this, true);
1328             } else {
1329                 replaceFolderWithFinalItem();
1330             }
1331         }
1332     }
1333 
getViewForInfo(final ShortcutInfo item)1334     private View getViewForInfo(final ShortcutInfo item) {
1335         return mContent.iterateOverItems(new ItemOperator() {
1336 
1337             @Override
1338             public boolean evaluate(ItemInfo info, View view, View parent) {
1339                 return info == item;
1340             }
1341         });
1342     }
1343 
1344     public void onItemsChanged() {
1345         updateTextViewFocus();
1346     }
1347 
1348     public void onTitleChanged(CharSequence title) {
1349     }
1350 
1351     public ArrayList<View> getItemsInReadingOrder() {
1352         if (mItemsInvalidated) {
1353             mItemsInReadingOrder.clear();
1354             mContent.iterateOverItems(new ItemOperator() {
1355 
1356                 @Override
1357                 public boolean evaluate(ItemInfo info, View view, View parent) {
1358                     mItemsInReadingOrder.add(view);
1359                     return false;
1360                 }
1361             });
1362             mItemsInvalidated = false;
1363         }
1364         return mItemsInReadingOrder;
1365     }
1366 
1367     public void getLocationInDragLayer(int[] loc) {
1368         mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
1369     }
1370 
1371     public void onFocusChange(View v, boolean hasFocus) {
1372         if (v == mFolderName) {
1373             if (hasFocus) {
1374                 startEditingFolderName();
1375             } else {
1376                 dismissEditingName();
1377             }
1378         }
1379     }
1380 
1381     @Override
1382     public void getHitRectRelativeToDragLayer(Rect outRect) {
1383         getHitRect(outRect);
1384         outRect.left -= mScrollAreaOffset;
1385         outRect.right += mScrollAreaOffset;
1386     }
1387 
1388     @Override
1389     public void fillInLaunchSourceData(View v, Bundle sourceData) {
1390         // Fill in from the folder icon's launch source provider first
1391         Stats.LaunchSourceUtils.populateSourceDataFromAncestorProvider(mFolderIcon, sourceData);
1392         sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_FOLDER);
1393         sourceData.putInt(Stats.SOURCE_EXTRA_SUB_CONTAINER_PAGE, mContent.getCurrentPage());
1394     }
1395 
1396     private class OnScrollHintListener implements OnAlarmListener {
1397 
1398         private final DragObject mDragObject;
1399 
1400         OnScrollHintListener(DragObject object) {
1401             mDragObject = object;
1402         }
1403 
1404         /**
1405          * Scroll hint has been shown long enough. Now scroll to appropriate page.
1406          */
1407         @Override
1408         public void onAlarm(Alarm alarm) {
1409             if (mCurrentScrollDir == DragController.SCROLL_LEFT) {
1410                 mContent.scrollLeft();
1411                 mScrollHintDir = DragController.SCROLL_NONE;
1412             } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) {
1413                 mContent.scrollRight();
1414                 mScrollHintDir = DragController.SCROLL_NONE;
1415             } else {
1416                 // This should not happen
1417                 return;
1418             }
1419             mCurrentScrollDir = DragController.SCROLL_NONE;
1420 
1421             // Pause drag event until the scrolling is finished
1422             mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject));
1423             mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY);
1424         }
1425     }
1426 
1427     private class OnScrollFinishedListener implements OnAlarmListener {
1428 
1429         private final DragObject mDragObject;
1430 
1431         OnScrollFinishedListener(DragObject object) {
1432             mDragObject = object;
1433         }
1434 
1435         /**
1436          * Page scroll is complete.
1437          */
1438         @Override
1439         public void onAlarm(Alarm alarm) {
1440             // Reorder immediately on page change.
1441             onDragOver(mDragObject, 1);
1442         }
1443     }
1444 
1445     // Compares item position based on rank and position giving priority to the rank.
1446     public static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() {
1447 
1448         @Override
1449         public int compare(ItemInfo lhs, ItemInfo rhs) {
1450             if (lhs.rank != rhs.rank) {
1451                 return lhs.rank - rhs.rank;
1452             } else if (lhs.cellY != rhs.cellY) {
1453                 return lhs.cellY - rhs.cellY;
1454             } else {
1455                 return lhs.cellX - rhs.cellX;
1456             }
1457         }
1458     };
1459 }
1460