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.content.Context;
20 import android.graphics.Canvas;
21 import android.util.AttributeSet;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.widget.LinearLayout;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import com.android.launcher3.BubbleTextView;
31 import com.android.launcher3.DeviceProfile;
32 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
33 import com.android.launcher3.Flags;
34 import com.android.launcher3.LauncherPrefs;
35 import com.android.launcher3.R;
36 import com.android.launcher3.Utilities;
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.keyboard.FocusIndicatorHelper;
41 import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
42 import com.android.launcher3.model.data.ItemInfo;
43 import com.android.launcher3.model.data.ItemInfoWithIcon;
44 import com.android.launcher3.model.data.WorkspaceItemInfo;
45 import com.android.launcher3.views.ActivityContext;
46 
47 import java.io.PrintWriter;
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.stream.Collectors;
51 
52 public class PredictionRowView<T extends Context & ActivityContext>
53         extends LinearLayout implements OnDeviceProfileChangeListener, FloatingHeaderRow {
54 
55     private final T mActivityContext;
56     private int mNumPredictedAppsPerRow;
57     // Vertical padding of the icon that contributes to the expected cell height.
58     private final int mVerticalPadding;
59     // Extra padding that is used in the top app rows (prediction and search) that is not used in
60     // the regular A-Z list. This only applies to single line label.
61     private final int mTopRowExtraHeight;
62 
63     // Helper to drawing the focus indicator.
64     private final FocusIndicatorHelper mFocusHelper;
65 
66     // The set of predicted apps resolved from the component names and the current set of apps
67     private final List<WorkspaceItemInfo> mPredictedApps = new ArrayList<>();
68 
69     private FloatingHeaderView mParent;
70 
71     private boolean mPredictionsEnabled = false;
72 
73     private boolean mPredictionUiUpdatePaused = false;
74 
PredictionRowView(@onNull Context context)75     public PredictionRowView(@NonNull Context context) {
76         this(context, null);
77     }
78 
PredictionRowView(@onNull Context context, @Nullable AttributeSet attrs)79     public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) {
80         super(context, attrs);
81         setOrientation(LinearLayout.HORIZONTAL);
82 
83         mFocusHelper = new SimpleFocusIndicatorHelper(this);
84         mActivityContext = ActivityContext.lookupContext(context);
85         mNumPredictedAppsPerRow = mActivityContext.getDeviceProfile().numShownAllAppsColumns;
86         mTopRowExtraHeight = getResources().getDimensionPixelSize(
87                 R.dimen.all_apps_search_top_row_extra_height);
88         mVerticalPadding = getResources().getDimensionPixelSize(
89                 R.dimen.all_apps_predicted_icon_vertical_padding);
90         updateVisibility();
91     }
92 
93     @Override
onAttachedToWindow()94     protected void onAttachedToWindow() {
95         super.onAttachedToWindow();
96         mActivityContext.addOnDeviceProfileChangeListener(this);
97     }
98 
99     @Override
onDetachedFromWindow()100     protected void onDetachedFromWindow() {
101         super.onDetachedFromWindow();
102         mActivityContext.removeOnDeviceProfileChangeListener(this);
103     }
104 
setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden)105     public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
106         mParent = parent;
107     }
108 
updateVisibility()109     private void updateVisibility() {
110         setVisibility(mPredictionsEnabled ? VISIBLE : GONE);
111         if (mActivityContext.getAppsView() != null) {
112             if (mPredictionsEnabled) {
113                 mActivityContext.getAppsView().getAppsStore().registerIconContainer(this);
114             } else {
115                 mActivityContext.getAppsView().getAppsStore().unregisterIconContainer(this);
116             }
117         }
118     }
119 
120     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)121     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
122         super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(),
123                 MeasureSpec.EXACTLY));
124     }
125 
126     @Override
dispatchDraw(Canvas canvas)127     protected void dispatchDraw(Canvas canvas) {
128         mFocusHelper.draw(canvas);
129         super.dispatchDraw(canvas);
130     }
131 
132     @Override
getExpectedHeight()133     public int getExpectedHeight() {
134         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
135         int iconHeight = deviceProfile.allAppsIconSizePx;
136         int iconPadding = deviceProfile.allAppsIconDrawablePaddingPx;
137         int textHeight = Utilities.calculateTextHeight(deviceProfile.allAppsIconTextSizePx);
138         int totalHeight = iconHeight + iconPadding + textHeight + mVerticalPadding * 2;
139         // Prediction row height will be 4dp bigger than the regular apps in A-Z list when two line
140         // is not enabled. Otherwise, the extra height will increase by just the textHeight.
141         int extraHeight = (Flags.enableTwolineToggle() &&
142                 LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext()))
143                 ? textHeight : mTopRowExtraHeight;
144         totalHeight += extraHeight;
145         return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom();
146     }
147 
148     @Override
shouldDraw()149     public boolean shouldDraw() {
150         return getVisibility() != GONE;
151     }
152 
153     @Override
hasVisibleContent()154     public boolean hasVisibleContent() {
155         return mPredictionsEnabled;
156     }
157 
158     @Override
isVisible()159     public boolean isVisible() {
160         return getVisibility() == VISIBLE;
161     }
162 
163     /**
164      * Returns the predicted apps.
165      */
getPredictedApps()166     public List<ItemInfoWithIcon> getPredictedApps() {
167         return new ArrayList<>(mPredictedApps);
168     }
169 
170     /**
171      * Sets the current set of predicted apps.
172      *
173      * This can be called before we get the full set of applications, we should merge the results
174      * only in onPredictionsUpdated() which is idempotent.
175      *
176      * If the number of predicted apps is the same as the previous list of predicted apps,
177      * we can optimize by swapping them in place.
178      */
setPredictedApps(List<ItemInfo> items)179     public void setPredictedApps(List<ItemInfo> items) {
180         applyPredictedApps(items);
181     }
182 
applyPredictedApps(List<ItemInfo> items)183     private void applyPredictedApps(List<ItemInfo> items) {
184         mPredictedApps.clear();
185         mPredictedApps.addAll(items.stream()
186                 .filter(itemInfo -> itemInfo instanceof WorkspaceItemInfo)
187                 .map(itemInfo -> (WorkspaceItemInfo) itemInfo).collect(Collectors.toList()));
188         applyPredictionApps();
189     }
190 
191     @Override
onDeviceProfileChanged(DeviceProfile dp)192     public void onDeviceProfileChanged(DeviceProfile dp) {
193         mNumPredictedAppsPerRow = dp.numShownAllAppsColumns;
194         removeAllViews();
195         applyPredictionApps();
196     }
197 
198     /** Pause the prediction row UI update */
setPredictionUiUpdatePaused(boolean predictionUiUpdatePaused)199     public void setPredictionUiUpdatePaused(boolean predictionUiUpdatePaused) {
200         mPredictionUiUpdatePaused = predictionUiUpdatePaused;
201         if (!mPredictionUiUpdatePaused) {
202             applyPredictionApps();
203         }
204     }
205 
applyPredictionApps()206     private void applyPredictionApps() {
207         if (mPredictionUiUpdatePaused) {
208             return;
209         }
210         if (getChildCount() != mNumPredictedAppsPerRow) {
211             while (getChildCount() > mNumPredictedAppsPerRow) {
212                 removeViewAt(0);
213             }
214             LayoutInflater inflater = mActivityContext.getAppsView().getLayoutInflater();
215             while (getChildCount() < mNumPredictedAppsPerRow) {
216                 BubbleTextView icon = (BubbleTextView) inflater.inflate(
217                         R.layout.all_apps_prediction_row_icon, this, false);
218                 icon.setOnClickListener(mActivityContext.getItemOnClickListener());
219                 icon.setOnLongClickListener(mActivityContext.getAllAppsItemLongClickListener());
220                 icon.setLongPressTimeoutFactor(1f);
221                 icon.setOnFocusChangeListener(mFocusHelper);
222 
223                 LayoutParams lp = (LayoutParams) icon.getLayoutParams();
224                 if (Flags.enableFocusOutline()) {
225                     lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
226                 } else {
227                     // Ensure the all apps icon height matches the workspace icons in portrait mode.
228                     lp.height = mActivityContext.getDeviceProfile().allAppsCellHeightPx;
229                 }
230                 lp.width = 0;
231                 lp.weight = 1;
232                 addView(icon);
233             }
234         }
235 
236         int predictionCount = mPredictedApps.size();
237 
238         for (int i = 0; i < getChildCount(); i++) {
239             BubbleTextView icon = (BubbleTextView) getChildAt(i);
240             icon.reset();
241             if (predictionCount > i) {
242                 icon.setVisibility(View.VISIBLE);
243                 WorkspaceItemInfo predictedItem = mPredictedApps.get(i);
244                 predictedItem.rank = i;
245                 predictedItem.cellX = i;
246                 predictedItem.cellY = 0;
247                 icon.applyFromWorkspaceItem(predictedItem);
248             } else {
249                 icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE);
250             }
251         }
252 
253         boolean predictionsEnabled = predictionCount > 0;
254         if (predictionsEnabled != mPredictionsEnabled) {
255             mPredictionsEnabled = predictionsEnabled;
256             updateVisibility();
257         }
258         mParent.onHeightUpdated();
259     }
260 
261     @Override
hasOverlappingRendering()262     public boolean hasOverlappingRendering() {
263         return false;
264     }
265 
266 
267     @Override
setVerticalScroll(int scroll, boolean isScrolledOut)268     public void setVerticalScroll(int scroll, boolean isScrolledOut) {
269         if (!isScrolledOut) {
270             setTranslationY(scroll);
271         }
272         setAlpha(isScrolledOut ? 0 : 1);
273         if (getVisibility() != GONE) {
274             AlphaUpdateListener.updateVisibility(this);
275         }
276     }
277 
278     @Override
getTypeClass()279     public Class<PredictionRowView> getTypeClass() {
280         return PredictionRowView.class;
281     }
282 
283     @Override
getFocusedChild()284     public View getFocusedChild() {
285         return getChildAt(0);
286     }
287 
dump(String prefix, PrintWriter writer)288     public void dump(String prefix, PrintWriter writer) {
289         writer.println(prefix + "PredictionRowView");
290         writer.println(prefix + "\tmPredictionsEnabled: " + mPredictionsEnabled);
291         writer.println(prefix + "\tmPredictionUiUpdatePaused: " + mPredictionUiUpdatePaused);
292         writer.println(prefix + "\tmNumPredictedAppsPerRow: " + mNumPredictedAppsPerRow);
293         writer.println(prefix + "\tmPredictedApps: " + mPredictedApps);
294     }
295 }
296