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 static android.animation.ValueAnimator.areAnimatorsEnabled;
19 
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.drawable.Drawable;
23 import android.os.Bundle;
24 import android.text.TextUtils;
25 import android.util.AttributeSet;
26 import android.view.View;
27 import android.view.accessibility.AccessibilityNodeInfo;
28 import android.widget.ImageView;
29 import android.widget.LinearLayout;
30 import android.widget.TextView;
31 
32 import androidx.annotation.Nullable;
33 import androidx.annotation.UiThread;
34 
35 import com.android.launcher3.DeviceProfile;
36 import com.android.launcher3.LauncherAppState;
37 import com.android.launcher3.R;
38 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
39 import com.android.launcher3.icons.PlaceHolderIconDrawable;
40 import com.android.launcher3.model.data.ItemInfoWithIcon;
41 import com.android.launcher3.model.data.PackageItemInfo;
42 import com.android.launcher3.util.CancellableTask;
43 import com.android.launcher3.views.ActivityContext;
44 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
45 
46 /**
47  * A UI represents a header of an app shown in the full widgets tray.
48  *
49  * It is a {@link LinearLayout} which contains an app icon, an app name, a subtitle and a checkbox
50  * which indicates if the widgets content view underneath this header should be shown.
51  */
52 public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver {
53 
54     private static final int[] EXPANDED_DRAWABLE_STATE = new int[] {android.R.attr.state_expanded};
55 
56     private final int mIconSize;
57     /**
58      * Indicates if the header is collapsable. For example, when displayed in a two pane layout,
59      * widget apps aren't collapsable.
60     */
61     private final boolean mIsCollapsable;
62     @Nullable private CancellableTask mIconLoadRequest;
63     @Nullable private Drawable mIconDrawable;
64     @Nullable private WidgetsListDrawableState mListDrawableState;
65     private ImageView mAppIcon;
66     private TextView mTitle;
67     private TextView mSubtitle;
68     private boolean mEnableIconUpdateAnimation = false;
69     private boolean mIsExpanded = false;
70 
WidgetsListHeader(Context context)71     public WidgetsListHeader(Context context) {
72         this(context, /* attrs= */ null);
73     }
74 
WidgetsListHeader(Context context, @Nullable AttributeSet attrs)75     public WidgetsListHeader(Context context, @Nullable AttributeSet attrs) {
76         this(context, attrs, /* defStyle= */ 0);
77     }
78 
WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr)79     public WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
80         super(context, attrs, defStyleAttr);
81 
82         ActivityContext activity = ActivityContext.lookupContext(context);
83         DeviceProfile grid = activity.getDeviceProfile();
84         TypedArray a = context.obtainStyledAttributes(attrs,
85                 R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0);
86         mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize,
87                 grid.iconSizePx);
88         mIsCollapsable = a.getBoolean(R.styleable.WidgetsListRowHeader_collapsable, true);
89     }
90 
91     @Override
onFinishInflate()92     protected void onFinishInflate() {
93         super.onFinishInflate();
94         mAppIcon = findViewById(R.id.app_icon);
95         mTitle = findViewById(R.id.app_title);
96         mSubtitle = findViewById(R.id.app_subtitle);
97         // Lists that cannot collapse, don't need EXPAND or COLLAPSE accessibility actions.
98         if (mIsCollapsable) {
99             setAccessibilityDelegate(new AccessibilityDelegate() {
100 
101                 @Override
102                 public void onInitializeAccessibilityNodeInfo(View host,
103                         AccessibilityNodeInfo info) {
104                     if (mIsExpanded) {
105                         info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND);
106                         info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
107                     } else {
108                         info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
109                         info.addAction(AccessibilityNodeInfo.ACTION_EXPAND);
110                     }
111                     super.onInitializeAccessibilityNodeInfo(host, info);
112                 }
113 
114                 @Override
115                 public boolean performAccessibilityAction(View host, int action, Bundle args) {
116                     switch (action) {
117                         case AccessibilityNodeInfo.ACTION_EXPAND:
118                         case AccessibilityNodeInfo.ACTION_COLLAPSE:
119                             callOnClick();
120                             return true;
121                         default:
122                             return super.performAccessibilityAction(host, action, args);
123                     }
124                 }
125             });
126         }
127     }
128 
129     /** Sets the expand toggle to expand / collapse. */
130     @UiThread
setExpanded(boolean isExpanded)131     public void setExpanded(boolean isExpanded) {
132         this.mIsExpanded = isExpanded;
133         refreshDrawableState();
134     }
135 
136     /** @return true if this header is expanded. */
isExpanded()137     public boolean isExpanded() {
138         return mIsExpanded;
139     }
140 
141     /** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
142     @UiThread
setListDrawableState(WidgetsListDrawableState state)143     public void setListDrawableState(WidgetsListDrawableState state) {
144         if (state == mListDrawableState) return;
145         this.mListDrawableState = state;
146         refreshDrawableState();
147     }
148 
149     /** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */
150     @UiThread
applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry)151     public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) {
152         PackageItemInfo info = entry.mPkgItem;
153         setIcon(info.newIcon(getContext()));
154         setTitles(entry);
155         setExpanded(entry.isWidgetListShown());
156 
157         super.setTag(info);
158 
159         verifyHighRes();
160     }
161 
setIcon(Drawable icon)162     void setIcon(Drawable icon) {
163         applyDrawables(icon);
164         mIconDrawable = icon;
165         if (mIconDrawable != null) {
166             mIconDrawable.setVisible(
167                     /* visible= */ getWindowVisibility() == VISIBLE && isShown(),
168                     /* restart= */ false);
169         }
170     }
171 
applyDrawables(Drawable icon)172     private void applyDrawables(Drawable icon) {
173         icon.setBounds(0, 0, mIconSize, mIconSize);
174 
175         LinearLayout.LayoutParams layoutParams =
176                 (LinearLayout.LayoutParams) mAppIcon.getLayoutParams();
177         layoutParams.width = mIconSize;
178         layoutParams.height = mIconSize;
179         mAppIcon.setLayoutParams(layoutParams);
180         mAppIcon.setImageDrawable(icon);
181 
182         // If the current icon is a placeholder color, animate its update.
183         if (mIconDrawable != null
184                 && mIconDrawable instanceof PlaceHolderIconDrawable
185                 && mEnableIconUpdateAnimation) {
186             ((PlaceHolderIconDrawable) mIconDrawable).animateIconUpdate(icon);
187         }
188     }
189 
setTitles(WidgetsListHeaderEntry entry)190     private void setTitles(WidgetsListHeaderEntry entry) {
191         mTitle.setText(entry.mPkgItem.title);
192 
193         String subtitle = entry.getSubtitle(getContext());
194         if (TextUtils.isEmpty(subtitle)) {
195             mSubtitle.setVisibility(GONE);
196         } else {
197             mSubtitle.setText(subtitle);
198             mSubtitle.setVisibility(VISIBLE);
199         }
200     }
201 
202     @Override
reapplyItemInfo(ItemInfoWithIcon info)203     public void reapplyItemInfo(ItemInfoWithIcon info) {
204         if (getTag() == info) {
205             mIconLoadRequest = null;
206             mEnableIconUpdateAnimation = areAnimatorsEnabled();
207 
208             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
209             info.bitmap.icon.prepareToDraw();
210 
211             setIcon(info.newIcon(getContext()));
212 
213             mEnableIconUpdateAnimation = false;
214         }
215     }
216 
217     @Override
onCreateDrawableState(int extraSpace)218     protected int[] onCreateDrawableState(int extraSpace) {
219         // We create a drawable state with an additional two spaces to be able to fit expanded state
220         // and the list drawable state.
221         int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
222         if (mIsExpanded) {
223             mergeDrawableStates(drawableState, EXPANDED_DRAWABLE_STATE);
224         }
225         if (mListDrawableState != null) {
226             mergeDrawableStates(drawableState, mListDrawableState.mStateSet);
227         }
228         return drawableState;
229     }
230 
231     /** Verifies that the current icon is high-res otherwise posts a request to load the icon. */
verifyHighRes()232     public void verifyHighRes() {
233         if (mIconLoadRequest != null) {
234             mIconLoadRequest.cancel();
235             mIconLoadRequest = null;
236         }
237         if (getTag() instanceof ItemInfoWithIcon) {
238             ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
239             if (info.usingLowResIcon()) {
240                 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
241                         .updateIconInBackground(this, info);
242             }
243         }
244     }
245 }
246