1 /* 2 * Copyright (C) 2021 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.widget.picker; 17 18 import android.content.Context; 19 import android.content.res.Resources; 20 import android.content.res.TypedArray; 21 import android.graphics.drawable.Drawable; 22 import android.os.Bundle; 23 import android.util.AttributeSet; 24 import android.view.View; 25 import android.view.accessibility.AccessibilityNodeInfo; 26 import android.widget.CheckBox; 27 import android.widget.ImageView; 28 import android.widget.LinearLayout; 29 import android.widget.TextView; 30 31 import androidx.annotation.Nullable; 32 import androidx.annotation.UiThread; 33 34 import com.android.launcher3.DeviceProfile; 35 import com.android.launcher3.LauncherAppState; 36 import com.android.launcher3.R; 37 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; 38 import com.android.launcher3.icons.PlaceHolderIconDrawable; 39 import com.android.launcher3.icons.cache.HandlerRunnable; 40 import com.android.launcher3.model.data.ItemInfoWithIcon; 41 import com.android.launcher3.model.data.PackageItemInfo; 42 import com.android.launcher3.views.ActivityContext; 43 import com.android.launcher3.widget.model.WidgetsListHeaderEntry; 44 import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; 45 46 import java.util.stream.Collectors; 47 48 /** 49 * A UI represents a header of an app shown in the full widgets tray. 50 * 51 * It is a {@link LinearLayout} which contains an app icon, an app name, a subtitle and a checkbox 52 * which indicates if the widgets content view underneath this header should be shown. 53 */ 54 public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver { 55 56 private boolean mEnableIconUpdateAnimation = false; 57 58 @Nullable private HandlerRunnable mIconLoadRequest; 59 @Nullable private Drawable mIconDrawable; 60 private final int mIconSize; 61 62 private ImageView mAppIcon; 63 private TextView mTitle; 64 private TextView mSubtitle; 65 66 private CheckBox mExpandToggle; 67 private boolean mIsExpanded = false; 68 @Nullable private WidgetsListDrawableState mListDrawableState; 69 WidgetsListHeader(Context context)70 public WidgetsListHeader(Context context) { 71 this(context, /* attrs= */ null); 72 } 73 WidgetsListHeader(Context context, @Nullable AttributeSet attrs)74 public WidgetsListHeader(Context context, @Nullable AttributeSet attrs) { 75 this(context, attrs, /* defStyle= */ 0); 76 } 77 WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr)78 public WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 79 super(context, attrs, defStyleAttr); 80 81 ActivityContext activity = ActivityContext.lookupContext(context); 82 DeviceProfile grid = activity.getDeviceProfile(); 83 TypedArray a = context.obtainStyledAttributes(attrs, 84 R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0); 85 mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize, 86 grid.iconSizePx); 87 } 88 89 @Override onFinishInflate()90 protected void onFinishInflate() { 91 super.onFinishInflate(); 92 mAppIcon = findViewById(R.id.app_icon); 93 mTitle = findViewById(R.id.app_title); 94 mSubtitle = findViewById(R.id.app_subtitle); 95 mExpandToggle = findViewById(R.id.toggle); 96 findViewById(R.id.app_container).setAccessibilityDelegate(new AccessibilityDelegate() { 97 98 @Override 99 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 100 if (mIsExpanded) { 101 info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND); 102 info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE); 103 } else { 104 info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE); 105 info.addAction(AccessibilityNodeInfo.ACTION_EXPAND); 106 } 107 super.onInitializeAccessibilityNodeInfo(host, info); 108 } 109 110 @Override 111 public boolean performAccessibilityAction(View host, int action, Bundle args) { 112 switch (action) { 113 case AccessibilityNodeInfo.ACTION_EXPAND: 114 case AccessibilityNodeInfo.ACTION_COLLAPSE: 115 callOnClick(); 116 return true; 117 default: 118 return super.performAccessibilityAction(host, action, args); 119 } 120 } 121 }); 122 } 123 124 /** 125 * Sets a {@link OnExpansionChangeListener} to get a callback when this app widgets section 126 * expands / collapses. 127 */ 128 @UiThread setOnExpandChangeListener( @ullable OnExpansionChangeListener onExpandChangeListener)129 public void setOnExpandChangeListener( 130 @Nullable OnExpansionChangeListener onExpandChangeListener) { 131 // Use the entire touch area of this view to expand / collapse an app widgets section. 132 setOnClickListener(view -> { 133 setExpanded(!mIsExpanded); 134 if (onExpandChangeListener != null) { 135 onExpandChangeListener.onExpansionChange(mIsExpanded); 136 } 137 }); 138 } 139 140 /** Sets the expand toggle to expand / collapse. */ 141 @UiThread setExpanded(boolean isExpanded)142 public void setExpanded(boolean isExpanded) { 143 this.mIsExpanded = isExpanded; 144 mExpandToggle.setChecked(isExpanded); 145 } 146 147 /** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */ 148 @UiThread setListDrawableState(WidgetsListDrawableState state)149 public void setListDrawableState(WidgetsListDrawableState state) { 150 if (state == mListDrawableState) return; 151 this.mListDrawableState = state; 152 refreshDrawableState(); 153 } 154 155 /** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */ 156 @UiThread applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry)157 public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) { 158 applyIconAndLabel(entry); 159 } 160 161 @UiThread applyIconAndLabel(WidgetsListHeaderEntry entry)162 private void applyIconAndLabel(WidgetsListHeaderEntry entry) { 163 PackageItemInfo info = entry.mPkgItem; 164 setIcon(info); 165 setTitles(entry); 166 setExpanded(entry.isWidgetListShown()); 167 168 super.setTag(info); 169 170 verifyHighRes(); 171 } 172 setIcon(PackageItemInfo info)173 private void setIcon(PackageItemInfo info) { 174 Drawable icon; 175 switch (info.category) { 176 case PackageItemInfo.CONVERSATIONS: 177 icon = getContext().getDrawable(R.drawable.ic_conversations_widget_category); 178 break; 179 default: 180 icon = info.newIcon(getContext()); 181 } 182 applyDrawables(icon); 183 mIconDrawable = icon; 184 if (mIconDrawable != null) { 185 mIconDrawable.setVisible( 186 /* visible= */ getWindowVisibility() == VISIBLE && isShown(), 187 /* restart= */ false); 188 } 189 } 190 applyDrawables(Drawable icon)191 private void applyDrawables(Drawable icon) { 192 icon.setBounds(0, 0, mIconSize, mIconSize); 193 194 LinearLayout.LayoutParams layoutParams = 195 (LinearLayout.LayoutParams) mAppIcon.getLayoutParams(); 196 layoutParams.width = mIconSize; 197 layoutParams.height = mIconSize; 198 mAppIcon.setLayoutParams(layoutParams); 199 mAppIcon.setImageDrawable(icon); 200 201 // If the current icon is a placeholder color, animate its update. 202 if (mIconDrawable != null 203 && mIconDrawable instanceof PlaceHolderIconDrawable 204 && mEnableIconUpdateAnimation) { 205 ((PlaceHolderIconDrawable) mIconDrawable).animateIconUpdate(icon); 206 } 207 } 208 setTitles(WidgetsListHeaderEntry entry)209 private void setTitles(WidgetsListHeaderEntry entry) { 210 mTitle.setText(entry.mPkgItem.title); 211 212 Resources resources = getContext().getResources(); 213 if (entry.widgetsCount == 0 && entry.shortcutsCount == 0) { 214 mSubtitle.setVisibility(GONE); 215 return; 216 } 217 218 String subtitle; 219 if (entry.widgetsCount > 0 && entry.shortcutsCount > 0) { 220 String widgetsCount = resources.getQuantityString(R.plurals.widgets_count, 221 entry.widgetsCount, entry.widgetsCount); 222 String shortcutsCount = resources.getQuantityString(R.plurals.shortcuts_count, 223 entry.shortcutsCount, entry.shortcutsCount); 224 subtitle = resources.getString(R.string.widgets_and_shortcuts_count, widgetsCount, 225 shortcutsCount); 226 } else if (entry.widgetsCount > 0) { 227 subtitle = resources.getQuantityString(R.plurals.widgets_count, 228 entry.widgetsCount, entry.widgetsCount); 229 } else { 230 subtitle = resources.getQuantityString(R.plurals.shortcuts_count, 231 entry.shortcutsCount, entry.shortcutsCount); 232 } 233 mSubtitle.setText(subtitle); 234 mSubtitle.setVisibility(VISIBLE); 235 } 236 237 /** Apply app icon, labels and tag using a generic {@link WidgetsListSearchHeaderEntry}. */ 238 @UiThread applyFromItemInfoWithIcon(WidgetsListSearchHeaderEntry entry)239 public void applyFromItemInfoWithIcon(WidgetsListSearchHeaderEntry entry) { 240 applyIconAndLabel(entry); 241 } 242 243 @UiThread applyIconAndLabel(WidgetsListSearchHeaderEntry entry)244 private void applyIconAndLabel(WidgetsListSearchHeaderEntry entry) { 245 PackageItemInfo info = entry.mPkgItem; 246 setIcon(info); 247 setTitles(entry); 248 setExpanded(entry.isWidgetListShown()); 249 250 super.setTag(info); 251 252 verifyHighRes(); 253 } 254 setTitles(WidgetsListSearchHeaderEntry entry)255 private void setTitles(WidgetsListSearchHeaderEntry entry) { 256 mTitle.setText(entry.mPkgItem.title); 257 258 mSubtitle.setText(entry.mWidgets.stream() 259 .map(item -> item.label).sorted().collect(Collectors.joining(", "))); 260 mSubtitle.setVisibility(VISIBLE); 261 } 262 263 @Override reapplyItemInfo(ItemInfoWithIcon info)264 public void reapplyItemInfo(ItemInfoWithIcon info) { 265 if (getTag() == info) { 266 mIconLoadRequest = null; 267 mEnableIconUpdateAnimation = true; 268 269 // Optimization: Starting in N, pre-uploads the bitmap to RenderThread. 270 info.bitmap.icon.prepareToDraw(); 271 272 setIcon((PackageItemInfo) info); 273 274 mEnableIconUpdateAnimation = false; 275 } 276 } 277 278 @Override onCreateDrawableState(int extraSpace)279 protected int[] onCreateDrawableState(int extraSpace) { 280 if (mListDrawableState == null) return super.onCreateDrawableState(extraSpace); 281 // Augment the state set from the super implementation with the custom states from 282 // mListDrawableState. 283 int[] drawableState = 284 super.onCreateDrawableState(extraSpace + mListDrawableState.mStateSet.length); 285 mergeDrawableStates(drawableState, mListDrawableState.mStateSet); 286 return drawableState; 287 } 288 289 /** Verifies that the current icon is high-res otherwise posts a request to load the icon. */ verifyHighRes()290 public void verifyHighRes() { 291 if (mIconLoadRequest != null) { 292 mIconLoadRequest.cancel(); 293 mIconLoadRequest = null; 294 } 295 if (getTag() instanceof ItemInfoWithIcon) { 296 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 297 if (info.usingLowResIcon()) { 298 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache() 299 .updateIconInBackground(this, info); 300 } 301 } 302 } 303 304 /** A listener for the widget section expansion / collapse events. */ 305 public interface OnExpansionChangeListener { 306 /** Notifies that the widget section is expanded or collapsed. */ onExpansionChange(boolean isExpanded)307 void onExpansionChange(boolean isExpanded); 308 } 309 } 310