• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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