1 /*
2  * Copyright (C) 2012 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.appprediction;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.graphics.Canvas;
22 import android.graphics.Rect;
23 import android.os.Build;
24 import android.util.AttributeSet;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.widget.LinearLayout;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 
32 import com.android.launcher3.BubbleTextView;
33 import com.android.launcher3.DeviceProfile;
34 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
35 import com.android.launcher3.Launcher;
36 import com.android.launcher3.R;
37 import com.android.launcher3.allapps.FloatingHeaderRow;
38 import com.android.launcher3.allapps.FloatingHeaderView;
39 import com.android.launcher3.anim.AlphaUpdateListener;
40 import com.android.launcher3.config.FeatureFlags;
41 import com.android.launcher3.keyboard.FocusIndicatorHelper;
42 import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
43 import com.android.launcher3.model.data.ItemInfo;
44 import com.android.launcher3.model.data.ItemInfoWithIcon;
45 import com.android.launcher3.model.data.WorkspaceItemInfo;
46 import com.android.launcher3.touch.ItemClickHandler;
47 import com.android.launcher3.touch.ItemLongClickListener;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 import java.util.stream.Collectors;
52 
53 @TargetApi(Build.VERSION_CODES.P)
54 public class PredictionRowView extends LinearLayout implements
55         OnDeviceProfileChangeListener, FloatingHeaderRow {
56 
57     private final Launcher mLauncher;
58     private int mNumPredictedAppsPerRow;
59 
60     // Helper to drawing the focus indicator.
61     private final FocusIndicatorHelper mFocusHelper;
62 
63     // The set of predicted apps resolved from the component names and the current set of apps
64     private final List<WorkspaceItemInfo> mPredictedApps = new ArrayList<>();
65 
66     private FloatingHeaderView mParent;
67     private boolean mScrolledOut;
68 
69     private boolean mPredictionsEnabled = false;
70 
71     @Nullable
72     private List<ItemInfo> mPendingPredictedItems;
73 
PredictionRowView(@onNull Context context)74     public PredictionRowView(@NonNull Context context) {
75         this(context, null);
76     }
77 
PredictionRowView(@onNull Context context, @Nullable AttributeSet attrs)78     public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) {
79         super(context, attrs);
80         setOrientation(LinearLayout.HORIZONTAL);
81 
82         mFocusHelper = new SimpleFocusIndicatorHelper(this);
83         mLauncher = Launcher.getLauncher(context);
84         mLauncher.addOnDeviceProfileChangeListener(this);
85         mNumPredictedAppsPerRow = mLauncher.getDeviceProfile().numShownAllAppsColumns;
86         updateVisibility();
87     }
88 
89     @Override
onAttachedToWindow()90     protected void onAttachedToWindow() {
91         super.onAttachedToWindow();
92     }
93 
setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden)94     public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
95         mParent = parent;
96     }
97 
updateVisibility()98     private void updateVisibility() {
99         setVisibility(mPredictionsEnabled ? VISIBLE : GONE);
100         if (mLauncher.getAppsView() != null) {
101             if (mPredictionsEnabled) {
102                 mLauncher.getAppsView().getAppsStore().registerIconContainer(this);
103             } else {
104                 mLauncher.getAppsView().getAppsStore().unregisterIconContainer(this);
105             }
106         }
107     }
108 
109     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)110     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
111         super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(),
112                 MeasureSpec.EXACTLY));
113     }
114 
115     @Override
dispatchDraw(Canvas canvas)116     protected void dispatchDraw(Canvas canvas) {
117         mFocusHelper.draw(canvas);
118         super.dispatchDraw(canvas);
119     }
120 
121     @Override
getExpectedHeight()122     public int getExpectedHeight() {
123         return getVisibility() == GONE ? 0 :
124                 Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx
125                         + getPaddingTop() + getPaddingBottom();
126     }
127 
128     @Override
shouldDraw()129     public boolean shouldDraw() {
130         return getVisibility() != GONE;
131     }
132 
133     @Override
hasVisibleContent()134     public boolean hasVisibleContent() {
135         return mPredictionsEnabled;
136     }
137 
138     @Override
isVisible()139     public boolean isVisible() {
140         return getVisibility() == VISIBLE;
141     }
142 
143     /**
144      * Returns the predicted apps.
145      */
getPredictedApps()146     public List<ItemInfoWithIcon> getPredictedApps() {
147         return new ArrayList<>(mPredictedApps);
148     }
149 
150     /**
151      * Sets the current set of predicted apps.
152      *
153      * This can be called before we get the full set of applications, we should merge the results
154      * only in onPredictionsUpdated() which is idempotent.
155      *
156      * If the number of predicted apps is the same as the previous list of predicted apps,
157      * we can optimize by swapping them in place.
158      */
setPredictedApps(List<ItemInfo> items)159     public void setPredictedApps(List<ItemInfo> items) {
160         if (!FeatureFlags.ENABLE_APP_PREDICTIONS_WHILE_VISIBLE.get()
161                 && !mLauncher.isWorkspaceLoading()
162                 && isShown()
163                 && getWindowVisibility() == View.VISIBLE) {
164             mPendingPredictedItems = items;
165             return;
166         }
167 
168         applyPredictedApps(items);
169     }
170 
applyPredictedApps(List<ItemInfo> items)171     private void applyPredictedApps(List<ItemInfo> items) {
172         mPendingPredictedItems = null;
173         mPredictedApps.clear();
174         mPredictedApps.addAll(items.stream()
175                 .filter(itemInfo -> itemInfo instanceof WorkspaceItemInfo)
176                 .map(itemInfo -> (WorkspaceItemInfo) itemInfo).collect(Collectors.toList()));
177         applyPredictionApps();
178     }
179 
180     @Override
onDeviceProfileChanged(DeviceProfile dp)181     public void onDeviceProfileChanged(DeviceProfile dp) {
182         mNumPredictedAppsPerRow = dp.numShownAllAppsColumns;
183         removeAllViews();
184         applyPredictionApps();
185     }
186 
applyPredictionApps()187     private void applyPredictionApps() {
188         if (getChildCount() != mNumPredictedAppsPerRow) {
189             while (getChildCount() > mNumPredictedAppsPerRow) {
190                 removeViewAt(0);
191             }
192             LayoutInflater inflater = mLauncher.getAppsView().getLayoutInflater();
193             while (getChildCount() < mNumPredictedAppsPerRow) {
194                 BubbleTextView icon = (BubbleTextView) inflater.inflate(
195                         R.layout.all_apps_icon, this, false);
196                 icon.setOnClickListener(ItemClickHandler.INSTANCE);
197                 icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS);
198                 icon.setLongPressTimeoutFactor(1f);
199                 icon.setOnFocusChangeListener(mFocusHelper);
200 
201                 LayoutParams lp = (LayoutParams) icon.getLayoutParams();
202                 // Ensure the all apps icon height matches the workspace icons in portrait mode.
203                 lp.height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
204                 lp.width = 0;
205                 lp.weight = 1;
206                 addView(icon);
207             }
208         }
209 
210         int predictionCount = mPredictedApps.size();
211 
212         for (int i = 0; i < getChildCount(); i++) {
213             BubbleTextView icon = (BubbleTextView) getChildAt(i);
214             icon.reset();
215             if (predictionCount > i) {
216                 icon.setVisibility(View.VISIBLE);
217                 icon.applyFromWorkspaceItem(mPredictedApps.get(i));
218             } else {
219                 icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE);
220             }
221         }
222 
223         boolean predictionsEnabled = predictionCount > 0;
224         if (predictionsEnabled != mPredictionsEnabled) {
225             mPredictionsEnabled = predictionsEnabled;
226             mLauncher.reapplyUi(false /* cancelCurrentAnimation */);
227             updateVisibility();
228         }
229         mParent.onHeightUpdated();
230     }
231 
232     @Override
hasOverlappingRendering()233     public boolean hasOverlappingRendering() {
234         return false;
235     }
236 
237 
238     @Override
setVerticalScroll(int scroll, boolean isScrolledOut)239     public void setVerticalScroll(int scroll, boolean isScrolledOut) {
240         mScrolledOut = isScrolledOut;
241         if (!isScrolledOut) {
242             setTranslationY(scroll);
243         }
244         setAlpha(mScrolledOut ? 0 : 1);
245         if (getVisibility() != GONE) {
246             AlphaUpdateListener.updateVisibility(this);
247         }
248     }
249 
250     @Override
setInsets(Rect insets, DeviceProfile grid)251     public void setInsets(Rect insets, DeviceProfile grid) {
252         int leftRightPadding = grid.allAppsLeftRightPadding;
253         setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
254     }
255 
256     @Override
getTypeClass()257     public Class<PredictionRowView> getTypeClass() {
258         return PredictionRowView.class;
259     }
260 
261     @Override
getFocusedChild()262     public View getFocusedChild() {
263         return getChildAt(0);
264     }
265 
266     @Override
onVisibilityAggregated(boolean isVisible)267     public void onVisibilityAggregated(boolean isVisible) {
268         super.onVisibilityAggregated(isVisible);
269 
270         if (mPendingPredictedItems != null && !isVisible) {
271             applyPredictedApps(mPendingPredictedItems);
272         }
273     }
274 }
275