AllAppsContainerView.java revision 60331a9be74a14051e6e192db69307ce652da2ae
1/*
2 * Copyright (C) 2015 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 */
16package com.android.launcher3.allapps;
17
18import android.annotation.SuppressLint;
19import android.annotation.TargetApi;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.res.Resources;
23import android.graphics.Point;
24import android.graphics.Rect;
25import android.graphics.drawable.InsetDrawable;
26import android.os.Build;
27import android.os.Bundle;
28import android.support.v7.widget.RecyclerView;
29import android.text.Selection;
30import android.text.SpannableStringBuilder;
31import android.text.method.TextKeyListener;
32import android.util.AttributeSet;
33import android.view.KeyEvent;
34import android.view.LayoutInflater;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.ViewConfiguration;
38import android.view.ViewGroup;
39import android.view.ViewTreeObserver;
40import android.widget.FrameLayout;
41import android.widget.LinearLayout;
42
43import com.android.launcher3.AppInfo;
44import com.android.launcher3.BaseContainerView;
45import com.android.launcher3.BubbleTextView;
46import com.android.launcher3.CellLayout;
47import com.android.launcher3.CheckLongPressHelper;
48import com.android.launcher3.DeleteDropTarget;
49import com.android.launcher3.DeviceProfile;
50import com.android.launcher3.DragSource;
51import com.android.launcher3.DropTarget;
52import com.android.launcher3.Folder;
53import com.android.launcher3.ItemInfo;
54import com.android.launcher3.Launcher;
55import com.android.launcher3.LauncherTransitionable;
56import com.android.launcher3.R;
57import com.android.launcher3.Stats;
58import com.android.launcher3.Utilities;
59import com.android.launcher3.Workspace;
60import com.android.launcher3.util.ComponentKey;
61import com.android.launcher3.util.Thunk;
62
63import java.nio.charset.Charset;
64import java.nio.charset.CharsetEncoder;
65import java.util.ArrayList;
66import java.util.List;
67
68
69
70/**
71 * A merge algorithm that merges every section indiscriminately.
72 */
73final class FullMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm {
74
75    @Override
76    public boolean continueMerging(AlphabeticalAppsList.SectionInfo section,
77           AlphabeticalAppsList.SectionInfo withSection,
78           int sectionAppCount, int numAppsPerRow, int mergeCount) {
79        // Merge EVERYTHING
80        return true;
81    }
82}
83
84/**
85 * The logic we use to merge multiple sections.  We only merge sections when their final row
86 * contains less than a certain number of icons, and stop at a specified max number of merges.
87 * In addition, we will try and not merge sections that identify apps from different scripts.
88 */
89final class SimpleSectionMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm {
90
91    private int mMinAppsPerRow;
92    private int mMinRowsInMergedSection;
93    private int mMaxAllowableMerges;
94    private CharsetEncoder mAsciiEncoder;
95
96    public SimpleSectionMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) {
97        mMinAppsPerRow = minAppsPerRow;
98        mMinRowsInMergedSection = minRowsInMergedSection;
99        mMaxAllowableMerges = maxNumMerges;
100        mAsciiEncoder = Charset.forName("US-ASCII").newEncoder();
101    }
102
103    @Override
104    public boolean continueMerging(AlphabeticalAppsList.SectionInfo section,
105           AlphabeticalAppsList.SectionInfo withSection,
106           int sectionAppCount, int numAppsPerRow, int mergeCount) {
107        // Continue merging if the number of hanging apps on the final row is less than some
108        // fixed number (ragged), the merged rows has yet to exceed some minimum row count,
109        // and while the number of merged sections is less than some fixed number of merges
110        int rows = sectionAppCount / numAppsPerRow;
111        int cols = sectionAppCount % numAppsPerRow;
112
113        // Ensure that we do not merge across scripts, currently we only allow for english and
114        // native scripts so we can test if both can just be ascii encoded
115        boolean isCrossScript = false;
116        if (section.firstAppItem != null && withSection.firstAppItem != null) {
117            isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) !=
118                    mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName);
119        }
120        return (0 < cols && cols < mMinAppsPerRow) &&
121                rows < mMinRowsInMergedSection &&
122                mergeCount < mMaxAllowableMerges &&
123                !isCrossScript;
124    }
125}
126
127/**
128 * The all apps view container.
129 */
130public class AllAppsContainerView extends BaseContainerView implements DragSource,
131        LauncherTransitionable, AlphabeticalAppsList.AdapterChangedCallback,
132        AllAppsGridAdapter.PredictionBarSpacerCallbacks, View.OnTouchListener,
133        View.OnLongClickListener, ViewTreeObserver.OnPreDrawListener,
134        AllAppsSearchBarController.Callbacks, Stats.LaunchSourceProvider {
135
136    private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
137    private static final int MAX_NUM_MERGES_PHONE = 2;
138
139    @Thunk Launcher mLauncher;
140    @Thunk AlphabeticalAppsList mApps;
141    private LayoutInflater mLayoutInflater;
142    private AllAppsGridAdapter mAdapter;
143    private RecyclerView.LayoutManager mLayoutManager;
144    private RecyclerView.ItemDecoration mItemDecoration;
145
146    @Thunk View mContent;
147    @Thunk View mContainerView;
148    @Thunk View mRevealView;
149    @Thunk AllAppsRecyclerView mAppsRecyclerView;
150    @Thunk ViewGroup mPredictionBarView;
151    @Thunk AllAppsSearchBarController mSearchBarController;
152    private ViewGroup mSearchBarContainerView;
153    private View mSearchBarView;
154
155    private int mSectionNamesMargin;
156    private int mNumAppsPerRow;
157    private int mNumPredictedAppsPerRow;
158    // This coordinate is relative to this container view
159    private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1);
160    // This coordinate is relative to its parent
161    private final Point mIconLastTouchPos = new Point();
162    // This coordinate is used to proxy click and long-click events to the prediction bar icons
163    private final Point mPredictionIconTouchDownPos = new Point();
164    // Normal container insets
165    private int mPredictionBarHeight;
166    private int mLastRecyclerViewScrollPos = -1;
167    @Thunk boolean mFocusPredictionBarOnFirstBind;
168
169    private SpannableStringBuilder mSearchQueryBuilder = null;
170
171    private CheckLongPressHelper mPredictionIconCheckForLongPress;
172    private View mPredictionIconUnderTouch;
173
174    public AllAppsContainerView(Context context) {
175        this(context, null);
176    }
177
178    public AllAppsContainerView(Context context, AttributeSet attrs) {
179        this(context, attrs, 0);
180    }
181
182    public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
183        super(context, attrs, defStyleAttr);
184        Resources res = context.getResources();
185
186        mLauncher = (Launcher) context;
187        mLayoutInflater = LayoutInflater.from(context);
188        DeviceProfile grid = mLauncher.getDeviceProfile();
189        mPredictionBarHeight = (int) (grid.allAppsIconSizePx + grid.iconDrawablePaddingOriginalPx +
190                Utilities.calculateTextHeight(grid.allAppsIconTextSizePx) +
191                2 * res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding) +
192                2 * res.getDimensionPixelSize(R.dimen.all_apps_prediction_bar_top_bottom_padding));
193        mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
194        mApps = new AlphabeticalAppsList(context);
195        mApps.setAdapterChangedCallback(this);
196        mAdapter = new AllAppsGridAdapter(context, mApps, this, this, mLauncher, this);
197        mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message));
198        mAdapter.setPredictionRowHeight(mPredictionBarHeight);
199        mApps.setAdapter(mAdapter);
200        mLayoutManager = mAdapter.getLayoutManager();
201        mItemDecoration = mAdapter.getItemDecoration();
202
203        mSearchQueryBuilder = new SpannableStringBuilder();
204        Selection.setSelection(mSearchQueryBuilder, 0);
205    }
206
207    /**
208     * Sets the current set of predicted apps.
209     */
210    public void setPredictedApps(List<ComponentName> apps) {
211        mApps.setPredictedApps(apps);
212    }
213
214    /**
215     * Sets the current set of apps.
216     */
217    public void setApps(List<AppInfo> apps) {
218        mApps.setApps(apps);
219    }
220
221    /**
222     * Adds new apps to the list.
223     */
224    public void addApps(List<AppInfo> apps) {
225        mApps.addApps(apps);
226    }
227
228    /**
229     * Updates existing apps in the list
230     */
231    public void updateApps(List<AppInfo> apps) {
232        mApps.updateApps(apps);
233    }
234
235    /**
236     * Removes some apps from the list.
237     */
238    public void removeApps(List<AppInfo> apps) {
239        mApps.removeApps(apps);
240    }
241
242    /**
243     * Sets the search bar that shows above the a-z list.
244     */
245    public void setSearchBarController(AllAppsSearchBarController searchController) {
246        if (mSearchBarController != null) {
247            throw new RuntimeException("Expected search bar controller to only be set once");
248        }
249        mSearchBarController = searchController;
250        mSearchBarController.initialize(mApps, this);
251
252        // Add the new search view to the layout
253        View searchBarView = searchController.getView(mSearchBarContainerView);
254        mSearchBarContainerView.addView(searchBarView);
255        mSearchBarContainerView.setVisibility(View.VISIBLE);
256        mSearchBarView = searchBarView;
257        setHasSearchBar();
258
259        updateBackgroundAndPaddings();
260    }
261
262    /**
263     * Scrolls this list view to the top.
264     */
265    public void scrollToTop() {
266        mAppsRecyclerView.scrollToTop();
267    }
268
269    /**
270     * Returns the content view used for the launcher transitions.
271     */
272    public View getContentView() {
273        return mContainerView;
274    }
275
276    /**
277     * Returns the all apps search view.
278     */
279    public View getSearchBarView() {
280        return mSearchBarView;
281    }
282
283    /**
284     * Returns the reveal view used for the launcher transitions.
285     */
286    public View getRevealView() {
287        return mRevealView;
288    }
289
290    /**
291     * Returns an new instance of the default app search controller.
292     */
293    public AllAppsSearchBarController newDefaultAppSearchController() {
294        return new DefaultAppSearchController(getContext(), this, mAppsRecyclerView);
295    }
296
297    @Override
298    protected void onFinishInflate() {
299        super.onFinishInflate();
300        boolean isRtl = Utilities.isRtl(getResources());
301        mAdapter.setRtl(isRtl);
302        mContent = findViewById(R.id.content);
303
304        // This is a focus listener that proxies focus from a view into the list view.  This is to
305        // work around the search box from getting first focus and showing the cursor.
306        View.OnFocusChangeListener focusProxyListener = new View.OnFocusChangeListener() {
307            @Override
308            public void onFocusChange(View v, boolean hasFocus) {
309                if (hasFocus) {
310                    if (!mApps.getPredictedApps().isEmpty()) {
311                        // If the prediction bar is going to be bound, then defer focusing until
312                        // it is first bound
313                        if (mPredictionBarView.getChildCount() == 0) {
314                            mFocusPredictionBarOnFirstBind = true;
315                        } else {
316                            mPredictionBarView.requestFocus();
317                        }
318                    } else {
319                        mAppsRecyclerView.requestFocus();
320                    }
321                }
322            }
323        };
324        mSearchBarContainerView = (ViewGroup) findViewById(R.id.search_box_container);
325        mSearchBarContainerView.setOnFocusChangeListener(focusProxyListener);
326        mContainerView = findViewById(R.id.all_apps_container);
327        mContainerView.setOnFocusChangeListener(focusProxyListener);
328        mRevealView = findViewById(R.id.all_apps_reveal);
329
330        // Load the all apps recycler view
331        mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
332        mAppsRecyclerView.setApps(mApps);
333        mAppsRecyclerView.setPredictionBarHeight(mPredictionBarHeight);
334        mAppsRecyclerView.setLayoutManager(mLayoutManager);
335        mAppsRecyclerView.setAdapter(mAdapter);
336        mAppsRecyclerView.setHasFixedSize(true);
337        if (mItemDecoration != null) {
338            mAppsRecyclerView.addItemDecoration(mItemDecoration);
339        }
340
341        // Fix the prediction bar height
342        mPredictionBarView = (ViewGroup) findViewById(R.id.prediction_bar);
343        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams();
344        lp.height = mPredictionBarHeight;
345
346        updateBackgroundAndPaddings();
347    }
348
349    @Override
350    public void onBoundsChanged(Rect newBounds) {
351        mLauncher.updateOverlayBounds(newBounds);
352    }
353
354    @Override
355    public void onBindPredictionBar() {
356        updatePredictionBarVisibility();
357
358        List<AppInfo> predictedApps = mApps.getPredictedApps();
359
360        // Remove extra prediction icons
361        while (mPredictionBarView.getChildCount() > mNumPredictedAppsPerRow) {
362            mPredictionBarView.removeViewAt(mPredictionBarView.getChildCount() - 1);
363        }
364
365        int childCount = mPredictionBarView.getChildCount();
366        for (int i = 0; i < mNumPredictedAppsPerRow; i++) {
367            BubbleTextView icon;
368            if (i < childCount) {
369                // If a child at that index exists, then get that child
370                icon = (BubbleTextView) mPredictionBarView.getChildAt(i);
371            } else {
372                // Otherwise, inflate a new icon
373                icon = (BubbleTextView) mLayoutInflater.inflate(
374                        R.layout.all_apps_prediction_bar_icon, mPredictionBarView, false);
375                icon.setFocusable(true);
376                icon.setLongPressTimeout(ViewConfiguration.get(getContext()).getLongPressTimeout());
377                mPredictionBarView.addView(icon);
378            }
379
380            // Either apply the app info to the child, or hide the view
381            if (i < predictedApps.size()) {
382                if (icon.getVisibility() != View.VISIBLE) {
383                    icon.setVisibility(View.VISIBLE);
384                }
385                icon.applyFromApplicationInfo(predictedApps.get(i));
386            } else {
387                icon.setVisibility(View.INVISIBLE);
388            }
389        }
390
391        if (mFocusPredictionBarOnFirstBind) {
392            mFocusPredictionBarOnFirstBind = false;
393            mPredictionBarView.requestFocus();
394        }
395    }
396
397    @Override
398    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
399        // Update the number of items in the grid before we measure the view
400        int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() :
401                MeasureSpec.getSize(widthMeasureSpec);
402        DeviceProfile grid = mLauncher.getDeviceProfile();
403        grid.updateAppsViewNumCols(getResources(), availableWidth);
404        if (mNumAppsPerRow != grid.allAppsNumCols ||
405                mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) {
406            mNumAppsPerRow = grid.allAppsNumCols;
407            mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols;
408
409            // If there is a start margin to draw section names, determine how we are going to merge
410            // app sections
411            boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone;
412            AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ?
413                    new FullMergeAlgorithm() :
414                    new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f),
415                            MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE);
416
417            mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
418            mAdapter.setNumAppsPerRow(mNumAppsPerRow);
419            mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm);
420        }
421
422        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
423    }
424
425    /**
426     * Update the background and padding of the Apps view and children.  Instead of insetting the
427     * container view, we inset the background and padding of the recycler view to allow for the
428     * recycler view to handle touch events (for fast scrolling) all the way to the edge.
429     */
430    @Override
431    protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) {
432        boolean isRtl = Utilities.isRtl(getResources());
433
434        // TODO: Use quantum_panel instead of quantum_panel_shape.
435        InsetDrawable background = new InsetDrawable(
436                getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0,
437                padding.right, 0);
438        mContainerView.setBackground(background);
439        mRevealView.setBackground(background.getConstantState().newDrawable());
440        mAppsRecyclerView.updateBackgroundPadding(padding);
441        mAdapter.updateBackgroundPadding(padding);
442
443        // Hack: We are going to let the recycler view take the full width, so reset the padding on
444        // the container to zero after setting the background and apply the top-bottom padding to
445        // the content view instead so that the launcher transition clips correctly.
446        mContent.setPadding(0, padding.top, 0, padding.bottom);
447        mContainerView.setPadding(0, 0, 0, 0);
448
449        // Pad the recycler view by the background padding plus the start margin (for the section
450        // names)
451        int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getScrollbarWidth());
452        if (isRtl) {
453            mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getScrollbarWidth(), 0,
454                    padding.right + startInset, 0);
455        } else {
456            mAppsRecyclerView.setPadding(padding.left + startInset, 0,
457                    padding.right + mAppsRecyclerView.getScrollbarWidth(), 0);
458        }
459
460        // Inset the search bar to fit its bounds above the container
461        if (mSearchBarView != null) {
462            Rect backgroundPadding = new Rect();
463            if (mSearchBarView.getBackground() != null) {
464                mSearchBarView.getBackground().getPadding(backgroundPadding);
465            }
466            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
467                    mSearchBarContainerView.getLayoutParams();
468            lp.leftMargin = searchBarBounds.left - backgroundPadding.left;
469            lp.topMargin = searchBarBounds.top - backgroundPadding.top;
470            lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) - backgroundPadding.right;
471            mSearchBarContainerView.requestLayout();
472        }
473
474        // Update the prediction bar insets as well
475        mPredictionBarView = (ViewGroup) findViewById(R.id.prediction_bar);
476        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams();
477        lp.leftMargin = padding.left + mAppsRecyclerView.getScrollbarWidth();
478        lp.rightMargin = padding.right + mAppsRecyclerView.getScrollbarWidth();
479        mPredictionBarView.requestLayout();
480    }
481
482    @Override
483    public boolean onPreDraw() {
484        if (mNumAppsPerRow > 0) {
485            // Update the position of the prediction bar to match the scroll of the all apps list
486            synchronizeToRecyclerViewScrollPosition(mAppsRecyclerView.getScrollPosition());
487        }
488        return true;
489    }
490
491    @Override
492    public boolean dispatchKeyEvent(KeyEvent event) {
493        // Determine if the key event was actual text, if so, focus the search bar and then dispatch
494        // the key normally so that it can process this key event
495        if (!mSearchBarController.isSearchFieldFocused() &&
496                event.getAction() == KeyEvent.ACTION_DOWN) {
497            final int unicodeChar = event.getUnicodeChar();
498            final boolean isKeyNotWhitespace = unicodeChar > 0 &&
499                    !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar);
500            if (isKeyNotWhitespace) {
501                boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder,
502                        event.getKeyCode(), event);
503                if (gotKey && mSearchQueryBuilder.length() > 0) {
504                    mSearchBarController.focusSearchField();
505                }
506            }
507        }
508
509        return super.dispatchKeyEvent(event);
510    }
511
512    @Override
513    public boolean onInterceptTouchEvent(MotionEvent ev) {
514        return handleTouchEvent(ev);
515    }
516
517    @SuppressLint("ClickableViewAccessibility")
518    @Override
519    public boolean onTouchEvent(MotionEvent ev) {
520        return handleTouchEvent(ev);
521    }
522
523    @SuppressLint("ClickableViewAccessibility")
524    @Override
525    public boolean onTouch(View v, MotionEvent ev) {
526        switch (ev.getAction()) {
527            case MotionEvent.ACTION_DOWN:
528            case MotionEvent.ACTION_MOVE:
529                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
530                break;
531        }
532        return false;
533    }
534
535    @Override
536    public boolean onLongClick(View v) {
537        // Return early if this is not initiated from a touch
538        if (!v.isInTouchMode()) return false;
539        // When we have exited all apps or are in transition, disregard long clicks
540        if (!mLauncher.isAppsViewVisible() ||
541                mLauncher.getWorkspace().isSwitchingState()) return false;
542        // Return if global dragging is not enabled
543        if (!mLauncher.isDraggingEnabled()) return false;
544
545        // Start the drag
546        mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false);
547        // Enter spring loaded mode
548        mLauncher.enterSpringLoadedDragMode();
549
550        return false;
551    }
552
553    @Override
554    public boolean supportsFlingToDelete() {
555        return true;
556    }
557
558    @Override
559    public boolean supportsAppInfoDropTarget() {
560        return true;
561    }
562
563    @Override
564    public boolean supportsDeleteDropTarget() {
565        return false;
566    }
567
568    @Override
569    public float getIntrinsicIconScaleFactor() {
570        DeviceProfile grid = mLauncher.getDeviceProfile();
571        return (float) grid.allAppsIconSizePx / grid.iconSizePx;
572    }
573
574    @Override
575    public void onFlingToDeleteCompleted() {
576        // We just dismiss the drag when we fling, so cleanup here
577        mLauncher.exitSpringLoadedDragModeDelayed(true,
578                Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
579        mLauncher.unlockScreenOrientation(false);
580    }
581
582    @Override
583    public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
584            boolean success) {
585        if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
586                !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
587            // Exit spring loaded mode if we have not successfully dropped or have not handled the
588            // drop in Workspace
589            mLauncher.exitSpringLoadedDragModeDelayed(true,
590                    Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
591        }
592        mLauncher.unlockScreenOrientation(false);
593
594        // Display an error message if the drag failed due to there not being enough space on the
595        // target layout we were dropping on.
596        if (!success) {
597            boolean showOutOfSpaceMessage = false;
598            if (target instanceof Workspace) {
599                int currentScreen = mLauncher.getCurrentWorkspaceScreen();
600                Workspace workspace = (Workspace) target;
601                CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen);
602                ItemInfo itemInfo = (ItemInfo) d.dragInfo;
603                if (layout != null) {
604                    layout.calculateSpans(itemInfo);
605                    showOutOfSpaceMessage =
606                            !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY);
607                }
608            }
609            if (showOutOfSpaceMessage) {
610                mLauncher.showOutOfSpaceMessage(false);
611            }
612
613            d.deferDragViewCleanupPostAnimation = false;
614        }
615    }
616
617    @Override
618    public void onAdapterItemsChanged() {
619        updatePredictionBarVisibility();
620    }
621
622    @Override
623    public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
624        // Register for a pre-draw listener to synchronize the recycler view scroll to other views
625        // in this container
626        if (!toWorkspace) {
627            getViewTreeObserver().addOnPreDrawListener(this);
628        }
629    }
630
631    @Override
632    public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
633        // Do nothing
634    }
635
636    @Override
637    public void onLauncherTransitionStep(Launcher l, float t) {
638        // Do nothing
639    }
640
641    @Override
642    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
643        if (toWorkspace) {
644            getViewTreeObserver().removeOnPreDrawListener(this);
645            mLastRecyclerViewScrollPos = -1;
646
647            // Reset the search bar after transitioning home
648            mSearchBarController.reset();
649        }
650    }
651
652    /**
653     * Updates the container when the recycler view is scrolled.
654     */
655    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
656    private void synchronizeToRecyclerViewScrollPosition(int scrollY) {
657        if (mLastRecyclerViewScrollPos != scrollY) {
658            mLastRecyclerViewScrollPos = scrollY;
659
660            // Scroll the prediction bar with the contents of the recycler view
661            mPredictionBarView.setTranslationY(-scrollY + mAppsRecyclerView.getPaddingTop());
662        }
663    }
664
665    @Override
666    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
667        // If we were waiting for long-click, cancel the request once a child has started handling
668        // the scrolling
669        if (mPredictionIconCheckForLongPress != null) {
670            mPredictionIconCheckForLongPress.cancelLongPress();
671        }
672        super.requestDisallowInterceptTouchEvent(disallowIntercept);
673    }
674
675    /**
676     * Handles the touch events to dismiss all apps when clicking outside the bounds of the
677     * recycler view.
678     */
679    private boolean handleTouchEvent(MotionEvent ev) {
680        DeviceProfile grid = mLauncher.getDeviceProfile();
681        int x = (int) ev.getX();
682        int y = (int) ev.getY();
683
684        switch (ev.getAction()) {
685            case MotionEvent.ACTION_DOWN:
686                // We workaround the fact that the recycler view needs the touches for the scroll
687                // and we want to intercept it for clicks in the prediction bar by handling clicks
688                // and long clicks in the prediction bar ourselves.
689                if (mPredictionBarView != null && mPredictionBarView.getVisibility() == View.VISIBLE) {
690                    mPredictionIconTouchDownPos.set(x, y);
691                    mPredictionIconUnderTouch = findPredictedAppAtCoordinate(x, y);
692                    if (mPredictionIconUnderTouch != null) {
693                        mPredictionIconCheckForLongPress =
694                                new CheckLongPressHelper(mPredictionIconUnderTouch, this);
695                        mPredictionIconCheckForLongPress.postCheckForLongPress();
696                    }
697                }
698
699                if (!mContentBounds.isEmpty()) {
700                    // Outset the fixed bounds and check if the touch is outside all apps
701                    Rect tmpRect = new Rect(mContentBounds);
702                    tmpRect.inset(-grid.allAppsIconSizePx / 2, 0);
703                    if (ev.getX() < tmpRect.left || ev.getX() > tmpRect.right) {
704                        mBoundsCheckLastTouchDownPos.set(x, y);
705                        return true;
706                    }
707                } else {
708                    // Check if the touch is outside all apps
709                    if (ev.getX() < getPaddingLeft() ||
710                            ev.getX() > (getWidth() - getPaddingRight())) {
711                        mBoundsCheckLastTouchDownPos.set(x, y);
712                        return true;
713                    }
714                }
715                break;
716            case MotionEvent.ACTION_MOVE:
717                if (mPredictionIconUnderTouch != null) {
718                    float dist = (float) Math.hypot(x - mPredictionIconTouchDownPos.x,
719                            y - mPredictionIconTouchDownPos.y);
720                    if (dist > ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
721                        if (mPredictionIconCheckForLongPress != null) {
722                            mPredictionIconCheckForLongPress.cancelLongPress();
723                        }
724                        mPredictionIconCheckForLongPress = null;
725                        mPredictionIconUnderTouch = null;
726                    }
727                }
728                break;
729            case MotionEvent.ACTION_UP:
730                if (mBoundsCheckLastTouchDownPos.x > -1) {
731                    ViewConfiguration viewConfig = ViewConfiguration.get(getContext());
732                    float dx = ev.getX() - mBoundsCheckLastTouchDownPos.x;
733                    float dy = ev.getY() - mBoundsCheckLastTouchDownPos.y;
734                    float distance = (float) Math.hypot(dx, dy);
735                    if (distance < viewConfig.getScaledTouchSlop()) {
736                        // The background was clicked, so just go home
737                        Launcher launcher = (Launcher) getContext();
738                        launcher.showWorkspace(true);
739                        return true;
740                    }
741                }
742
743                // Trigger the click on the prediction bar icon if that's where we touched
744                if (mPredictionIconUnderTouch != null &&
745                        !mPredictionIconCheckForLongPress.hasPerformedLongPress()) {
746                    mLauncher.onClick(mPredictionIconUnderTouch);
747                }
748
749                // Fall through
750            case MotionEvent.ACTION_CANCEL:
751                mBoundsCheckLastTouchDownPos.set(-1, -1);
752                mPredictionIconTouchDownPos.set(-1, -1);
753
754                // On touch up/cancel, cancel the long press on the prediction bar icon if it has
755                // not yet been performed
756                if (mPredictionIconCheckForLongPress != null) {
757                    mPredictionIconCheckForLongPress.cancelLongPress();
758                    mPredictionIconCheckForLongPress = null;
759                }
760                mPredictionIconUnderTouch = null;
761
762                break;
763        }
764        return false;
765    }
766
767    @Override
768    public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
769        if (apps != null) {
770            if (apps.isEmpty()) {
771                String formatStr = getResources().getString(R.string.all_apps_no_search_results);
772                mAdapter.setEmptySearchText(String.format(formatStr, query));
773            } else {
774                mAppsRecyclerView.scrollToTop();
775            }
776            mApps.setOrderedFilter(apps);
777        }
778    }
779
780    @Override
781    public void clearSearchResult() {
782        mApps.setOrderedFilter(null);
783
784        // Clear the search query
785        mSearchQueryBuilder.clear();
786        mSearchQueryBuilder.clearSpans();
787        Selection.setSelection(mSearchQueryBuilder, 0);
788    }
789
790    @Override
791    public void fillInLaunchSourceData(Bundle sourceData) {
792        // Since the other cases are caught by the AllAppsRecyclerView LaunchSourceProvider, we just
793        // handle the prediction bar icons here
794        sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
795        sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
796                Stats.SUB_CONTAINER_ALL_APPS_PREDICTION);
797    }
798
799    /**
800     * Returns the predicted app in the prediction bar given a set of local coordinates.
801     */
802    private View findPredictedAppAtCoordinate(int x, int y) {
803        Rect hitRect = new Rect();
804
805        // Ensure that are touching in the recycler view
806        int[] coord = {x, y};
807        Utilities.mapCoordInSelfToDescendent(mAppsRecyclerView, this, coord);
808        mAppsRecyclerView.getHitRect(hitRect);
809        if (!hitRect.contains(coord[0], coord[1])) {
810            return null;
811        }
812
813        // Check against the children of the prediction bar
814        coord[0] = x;
815        coord[1] = y;
816        Utilities.mapCoordInSelfToDescendent(mPredictionBarView, this, coord);
817        for (int i = 0; i < mPredictionBarView.getChildCount(); i++) {
818            View child = mPredictionBarView.getChildAt(i);
819            if (child.getVisibility() != View.VISIBLE) {
820                continue;
821            }
822            child.getHitRect(hitRect);
823            if (hitRect.contains(coord[0], coord[1])) {
824                return child;
825            }
826        }
827        return null;
828    }
829
830    /**
831     * Updates the visibility of the prediction bar.
832     * @return whether the prediction bar is visible
833     */
834    private boolean updatePredictionBarVisibility() {
835        boolean showPredictionBar = !mApps.getPredictedApps().isEmpty() &&
836                (!mApps.hasFilter() || mSearchBarController.shouldShowPredictionBar());
837        if (showPredictionBar) {
838            mPredictionBarView.setVisibility(View.VISIBLE);
839        } else if (!showPredictionBar) {
840            mPredictionBarView.setVisibility(View.INVISIBLE);
841        }
842        return showPredictionBar;
843    }
844}
845