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