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  */
16 package com.android.launcher3.allapps;
17 
18 import android.content.Context;
19 import android.graphics.Color;
20 import android.graphics.Rect;
21 import android.graphics.drawable.ColorDrawable;
22 import android.graphics.drawable.InsetDrawable;
23 import android.support.annotation.NonNull;
24 import android.support.annotation.Nullable;
25 import android.support.v7.widget.RecyclerView;
26 import android.text.Selection;
27 import android.text.Spannable;
28 import android.text.SpannableString;
29 import android.text.SpannableStringBuilder;
30 import android.text.TextUtils;
31 import android.text.method.TextKeyListener;
32 import android.util.AttributeSet;
33 import android.view.KeyEvent;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewGroup;
37 
38 import com.android.launcher3.AppInfo;
39 import com.android.launcher3.BaseContainerView;
40 import com.android.launcher3.BubbleTextView;
41 import com.android.launcher3.DeleteDropTarget;
42 import com.android.launcher3.DeviceProfile;
43 import com.android.launcher3.DragSource;
44 import com.android.launcher3.DropTarget;
45 import com.android.launcher3.ExtendedEditText;
46 import com.android.launcher3.Insettable;
47 import com.android.launcher3.ItemInfo;
48 import com.android.launcher3.Launcher;
49 import com.android.launcher3.R;
50 import com.android.launcher3.Utilities;
51 import com.android.launcher3.config.FeatureFlags;
52 import com.android.launcher3.discovery.AppDiscoveryItem;
53 import com.android.launcher3.discovery.AppDiscoveryUpdateState;
54 import com.android.launcher3.dragndrop.DragController;
55 import com.android.launcher3.dragndrop.DragOptions;
56 import com.android.launcher3.folder.Folder;
57 import com.android.launcher3.graphics.TintedDrawableSpan;
58 import com.android.launcher3.keyboard.FocusedItemDecorator;
59 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
60 import com.android.launcher3.util.ComponentKey;
61 import com.android.launcher3.util.PackageUserKey;
62 
63 import java.util.ArrayList;
64 import java.util.List;
65 import java.util.Set;
66 
67 /**
68  * The all apps view container.
69  */
70 public class AllAppsContainerView extends BaseContainerView implements DragSource,
71         View.OnLongClickListener, AllAppsSearchBarController.Callbacks, Insettable {
72 
73     private final Launcher mLauncher;
74     private final AlphabeticalAppsList mApps;
75     private final AllAppsGridAdapter mAdapter;
76     private final RecyclerView.LayoutManager mLayoutManager;
77 
78     private AllAppsRecyclerView mAppsRecyclerView;
79     private AllAppsSearchBarController mSearchBarController;
80 
81     private View mSearchContainer;
82     private int mSearchContainerMinHeight;
83     private ExtendedEditText mSearchInput;
84     private HeaderElevationController mElevationController;
85 
86     private SpannableStringBuilder mSearchQueryBuilder = null;
87 
88     private int mNumAppsPerRow;
89     private int mNumPredictedAppsPerRow;
90 
AllAppsContainerView(Context context)91     public AllAppsContainerView(Context context) {
92         this(context, null);
93     }
94 
AllAppsContainerView(Context context, AttributeSet attrs)95     public AllAppsContainerView(Context context, AttributeSet attrs) {
96         this(context, attrs, 0);
97     }
98 
AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr)99     public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
100         super(context, attrs, defStyleAttr);
101 
102         mLauncher = Launcher.getLauncher(context);
103         mApps = new AlphabeticalAppsList(context);
104         mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
105         mApps.setAdapter(mAdapter);
106         mLayoutManager = mAdapter.getLayoutManager();
107         mSearchQueryBuilder = new SpannableStringBuilder();
108         mSearchContainerMinHeight
109                 = getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_height);
110 
111         Selection.setSelection(mSearchQueryBuilder, 0);
112     }
113 
114     @Override
updateBackground( int paddingLeft, int paddingTop, int paddingRight, int paddingBottom)115     protected void updateBackground(
116             int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) {
117         if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
118             if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
119                 getRevealView().setBackground(new InsetDrawable(mBaseDrawable,
120                         paddingLeft, paddingTop, paddingRight, paddingBottom));
121                 getContentView().setBackground(
122                         new InsetDrawable(new ColorDrawable(Color.TRANSPARENT),
123                                 paddingLeft, paddingTop, paddingRight, paddingBottom));
124             } else {
125                 getRevealView().setBackground(mBaseDrawable);
126             }
127         } else {
128             super.updateBackground(paddingLeft, paddingTop, paddingRight, paddingBottom);
129         }
130     }
131 
132     /**
133      * Sets the current set of predicted apps.
134      */
setPredictedApps(List<ComponentKey> apps)135     public void setPredictedApps(List<ComponentKey> apps) {
136         mApps.setPredictedApps(apps);
137     }
138 
139     /**
140      * Sets the current set of apps.
141      */
setApps(List<AppInfo> apps)142     public void setApps(List<AppInfo> apps) {
143         mApps.setApps(apps);
144     }
145 
146     /**
147      * Adds new apps to the list.
148      */
addApps(List<AppInfo> apps)149     public void addApps(List<AppInfo> apps) {
150         mApps.addApps(apps);
151         mSearchBarController.refreshSearchResult();
152     }
153 
154     /**
155      * Updates existing apps in the list
156      */
updateApps(List<AppInfo> apps)157     public void updateApps(List<AppInfo> apps) {
158         mApps.updateApps(apps);
159         mSearchBarController.refreshSearchResult();
160     }
161 
162     /**
163      * Removes some apps from the list.
164      */
removeApps(List<AppInfo> apps)165     public void removeApps(List<AppInfo> apps) {
166         mApps.removeApps(apps);
167         mSearchBarController.refreshSearchResult();
168     }
169 
setSearchBarVisible(boolean visible)170     public void setSearchBarVisible(boolean visible) {
171         if (visible) {
172             mSearchBarController.setVisibility(View.VISIBLE);
173         } else {
174             mSearchBarController.setVisibility(View.INVISIBLE);
175         }
176     }
177 
178     /**
179      * Sets the search bar that shows above the a-z list.
180      */
setSearchBarController(AllAppsSearchBarController searchController)181     public void setSearchBarController(AllAppsSearchBarController searchController) {
182         if (mSearchBarController != null) {
183             throw new RuntimeException("Expected search bar controller to only be set once");
184         }
185         mSearchBarController = searchController;
186         mSearchBarController.initialize(mApps, mSearchInput, mLauncher, this);
187         mAdapter.setSearchController(mSearchBarController);
188     }
189 
190     /**
191      * Scrolls this list view to the top.
192      */
scrollToTop()193     public void scrollToTop() {
194         mAppsRecyclerView.scrollToTop();
195     }
196 
197     /**
198      * Returns whether the view itself will handle the touch event or not.
199      */
shouldContainerScroll(MotionEvent ev)200     public boolean shouldContainerScroll(MotionEvent ev) {
201         int[] point = new int[2];
202         point[0] = (int) ev.getX();
203         point[1] = (int) ev.getY();
204         Utilities.mapCoordInSelfToDescendant(mAppsRecyclerView, this, point);
205 
206         // IF the MotionEvent is inside the search box, and the container keeps on receiving
207         // touch input, container should move down.
208         if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) {
209             return true;
210         }
211 
212         // IF the MotionEvent is inside the thumb, container should not be pulled down.
213         if (mAppsRecyclerView.getScrollBar().isNearThumb(point[0], point[1])) {
214             return false;
215         }
216 
217         // IF scroller is at the very top OR there is no scroll bar because there is probably not
218         // enough items to scroll, THEN it's okay for the container to be pulled down.
219         if (mAppsRecyclerView.getCurrentScrollY() == 0) {
220             return true;
221         }
222         return false;
223     }
224 
225     /**
226      * Focuses the search field and begins an app search.
227      */
startAppsSearch()228     public void startAppsSearch() {
229         if (mSearchBarController != null) {
230             mSearchBarController.focusSearchField();
231         }
232     }
233 
234     /**
235      * Resets the state of AllApps.
236      */
reset()237     public void reset() {
238         // Reset the search bar and base recycler view after transitioning home
239         scrollToTop();
240         mSearchBarController.reset();
241         mAppsRecyclerView.reset();
242     }
243 
244     @Override
onFinishInflate()245     protected void onFinishInflate() {
246         super.onFinishInflate();
247 
248         // This is a focus listener that proxies focus from a view into the list view.  This is to
249         // work around the search box from getting first focus and showing the cursor.
250         getContentView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
251             @Override
252             public void onFocusChange(View v, boolean hasFocus) {
253                 if (hasFocus) {
254                     mAppsRecyclerView.requestFocus();
255                 }
256             }
257         });
258 
259         mSearchContainer = findViewById(R.id.search_container);
260         mSearchInput = (ExtendedEditText) findViewById(R.id.search_box_input);
261 
262         // Update the hint to contain the icon.
263         // Prefix the original hint with two spaces. The first space gets replaced by the icon
264         // using span. The second space is used for a singe space character between the hint
265         // and the icon.
266         SpannableString spanned = new SpannableString("  " + mSearchInput.getHint());
267         spanned.setSpan(new TintedDrawableSpan(getContext(), R.drawable.ic_allapps_search),
268                 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
269         mSearchInput.setHint(spanned);
270 
271         mElevationController = new HeaderElevationController(mSearchContainer);
272 
273         // Load the all apps recycler view
274         mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
275         mAppsRecyclerView.setApps(mApps);
276         mAppsRecyclerView.setLayoutManager(mLayoutManager);
277         mAppsRecyclerView.setAdapter(mAdapter);
278         mAppsRecyclerView.setHasFixedSize(true);
279         mAppsRecyclerView.addOnScrollListener(mElevationController);
280         mAppsRecyclerView.setElevationController(mElevationController);
281 
282         FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mAppsRecyclerView);
283         mAppsRecyclerView.addItemDecoration(focusedItemDecorator);
284         mAppsRecyclerView.preMeasureViews(mAdapter);
285         mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
286 
287         if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
288             getRevealView().setVisibility(View.VISIBLE);
289             getContentView().setVisibility(View.VISIBLE);
290             getContentView().setBackground(null);
291         }
292     }
293 
294     @Override
getTouchDelegateTargetView()295     public View getTouchDelegateTargetView() {
296         return mAppsRecyclerView;
297     }
298 
299     @Override
onBoundsChanged(Rect newBounds)300     public void onBoundsChanged(Rect newBounds) { }
301 
302     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)303     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
304         DeviceProfile grid = mLauncher.getDeviceProfile();
305         grid.updateAppsViewNumCols();
306         if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
307             if (mNumAppsPerRow != grid.inv.numColumns ||
308                     mNumPredictedAppsPerRow != grid.inv.numColumns) {
309                 mNumAppsPerRow = grid.inv.numColumns;
310                 mNumPredictedAppsPerRow = grid.inv.numColumns;
311 
312                 mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow);
313                 mAdapter.setNumAppsPerRow(mNumAppsPerRow);
314                 mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
315             }
316             if (!grid.isVerticalBarLayout()) {
317                 MarginLayoutParams searchContainerLp =
318                         (MarginLayoutParams) mSearchContainer.getLayoutParams();
319 
320                 searchContainerLp.height = mLauncher.getDragLayer().getInsets().top
321                         + mSearchContainerMinHeight;
322                 mSearchContainer.setLayoutParams(searchContainerLp);
323             }
324             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
325             return;
326         }
327 
328         // --- remove START when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. ---
329 
330         // Update the number of items in the grid before we measure the view
331         grid.updateAppsViewNumCols();
332         if (mNumAppsPerRow != grid.allAppsNumCols ||
333                 mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) {
334             mNumAppsPerRow = grid.allAppsNumCols;
335             mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols;
336 
337             mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow);
338             mAdapter.setNumAppsPerRow(mNumAppsPerRow);
339             mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
340         }
341 
342         // --- remove END when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. ---
343         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
344     }
345 
346     @Override
dispatchKeyEvent(KeyEvent event)347     public boolean dispatchKeyEvent(KeyEvent event) {
348         // Determine if the key event was actual text, if so, focus the search bar and then dispatch
349         // the key normally so that it can process this key event
350         if (!mSearchBarController.isSearchFieldFocused() &&
351                 event.getAction() == KeyEvent.ACTION_DOWN) {
352             final int unicodeChar = event.getUnicodeChar();
353             final boolean isKeyNotWhitespace = unicodeChar > 0 &&
354                     !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar);
355             if (isKeyNotWhitespace) {
356                 boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder,
357                         event.getKeyCode(), event);
358                 if (gotKey && mSearchQueryBuilder.length() > 0) {
359                     mSearchBarController.focusSearchField();
360                 }
361             }
362         }
363 
364         return super.dispatchKeyEvent(event);
365     }
366 
367     @Override
onLongClick(final View v)368     public boolean onLongClick(final View v) {
369         // Return early if this is not initiated from a touch
370         if (!v.isInTouchMode()) return false;
371         // When we have exited all apps or are in transition, disregard long clicks
372 
373         if (!mLauncher.isAppsViewVisible() ||
374                 mLauncher.getWorkspace().isSwitchingState()) return false;
375         // Return if global dragging is not enabled or we are already dragging
376         if (!mLauncher.isDraggingEnabled()) return false;
377         if (mLauncher.getDragController().isDragging()) return false;
378 
379         // Start the drag
380         final DragController dragController = mLauncher.getDragController();
381         dragController.addDragListener(new DragController.DragListener() {
382             @Override
383             public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
384                 v.setVisibility(INVISIBLE);
385             }
386 
387             @Override
388             public void onDragEnd() {
389                 v.setVisibility(VISIBLE);
390                 dragController.removeDragListener(this);
391             }
392         });
393         mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions());
394         return false;
395     }
396 
397     @Override
supportsAppInfoDropTarget()398     public boolean supportsAppInfoDropTarget() {
399         return true;
400     }
401 
402     @Override
supportsDeleteDropTarget()403     public boolean supportsDeleteDropTarget() {
404         return false;
405     }
406 
407     @Override
getIntrinsicIconScaleFactor()408     public float getIntrinsicIconScaleFactor() {
409         DeviceProfile grid = mLauncher.getDeviceProfile();
410         return (float) grid.allAppsIconSizePx / grid.iconSizePx;
411     }
412 
413     @Override
onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, boolean success)414     public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
415             boolean success) {
416         if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
417                 !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
418             // Exit spring loaded mode if we have not successfully dropped or have not handled the
419             // drop in Workspace
420             mLauncher.exitSpringLoadedDragModeDelayed(true,
421                     Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
422         }
423         mLauncher.unlockScreenOrientation(false);
424 
425         if (!success) {
426             d.deferDragViewCleanupPostAnimation = false;
427         }
428     }
429 
430     @Override
onSearchResult(String query, ArrayList<ComponentKey> apps)431     public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
432         if (apps != null) {
433             mApps.setOrderedFilter(apps);
434             mAppsRecyclerView.onSearchResultsChanged();
435             mAdapter.setLastSearchQuery(query);
436         }
437     }
438 
439     @Override
onAppDiscoverySearchUpdate(@ullable AppDiscoveryItem app, @NonNull AppDiscoveryUpdateState state)440     public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app,
441             @NonNull AppDiscoveryUpdateState state) {
442         if (!mLauncher.isDestroyed()) {
443             mApps.onAppDiscoverySearchUpdate(app, state);
444             mAppsRecyclerView.onSearchResultsChanged();
445         }
446     }
447 
448     @Override
clearSearchResult()449     public void clearSearchResult() {
450         if (mApps.setOrderedFilter(null)) {
451             mAppsRecyclerView.onSearchResultsChanged();
452         }
453 
454         // Clear the search query
455         mSearchQueryBuilder.clear();
456         mSearchQueryBuilder.clearSpans();
457         Selection.setSelection(mSearchQueryBuilder, 0);
458     }
459 
460     @Override
fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent)461     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
462         targetParent.containerType = mAppsRecyclerView.getContainerType(v);
463     }
464 
shouldRestoreImeState()465     public boolean shouldRestoreImeState() {
466         return !TextUtils.isEmpty(mSearchInput.getText());
467     }
468 
469     @Override
setInsets(Rect insets)470     public void setInsets(Rect insets) {
471         DeviceProfile grid = mLauncher.getDeviceProfile();
472         if (grid.isVerticalBarLayout()) {
473             ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
474             mlp.leftMargin = insets.left;
475             mlp.topMargin = insets.top;
476             mlp.rightMargin = insets.right;
477             setLayoutParams(mlp);
478         } else {
479             View navBarBg = findViewById(R.id.nav_bar_bg);
480             ViewGroup.LayoutParams navBarBgLp = navBarBg.getLayoutParams();
481             navBarBgLp.height = insets.bottom;
482             navBarBg.setLayoutParams(navBarBgLp);
483             navBarBg.setVisibility(View.VISIBLE);
484         }
485     }
486 
updateIconBadges(Set<PackageUserKey> updatedBadges)487     public void updateIconBadges(Set<PackageUserKey> updatedBadges) {
488         final PackageUserKey packageUserKey = new PackageUserKey(null, null);
489         final int n = mAppsRecyclerView.getChildCount();
490         for (int i = 0; i < n; i++) {
491             View child = mAppsRecyclerView.getChildAt(i);
492             if (!(child instanceof BubbleTextView) || !(child.getTag() instanceof ItemInfo)) {
493                 continue;
494             }
495             ItemInfo info = (ItemInfo) child.getTag();
496             if (packageUserKey.updateFromItemInfo(info) && updatedBadges.contains(packageUserKey)) {
497                 ((BubbleTextView) child).applyBadgeState(info, true /* animate */);
498             }
499         }
500     }
501 }
502