1 /*
2  * Copyright (C) 2023 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.apppairs;
18 
19 import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
20 
21 import android.content.Context;
22 import android.graphics.Paint;
23 import android.graphics.Rect;
24 import android.util.AttributeSet;
25 import android.view.LayoutInflater;
26 import android.view.ViewGroup;
27 import android.widget.FrameLayout;
28 
29 import androidx.annotation.Nullable;
30 
31 import com.android.launcher3.BubbleTextView;
32 import com.android.launcher3.DeviceProfile;
33 import com.android.launcher3.Flags;
34 import com.android.launcher3.Launcher;
35 import com.android.launcher3.LauncherAppState;
36 import com.android.launcher3.R;
37 import com.android.launcher3.Reorderable;
38 import com.android.launcher3.dragndrop.DraggableView;
39 import com.android.launcher3.icons.IconCache;
40 import com.android.launcher3.model.data.AppPairInfo;
41 import com.android.launcher3.model.data.ItemInfo;
42 import com.android.launcher3.util.MultiTranslateDelegate;
43 import com.android.launcher3.views.ActivityContext;
44 
45 import java.util.Comparator;
46 import java.util.function.Predicate;
47 
48 /**
49  * A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace.
50  * <br>
51  * The app pair icon is two parallel background rectangles with rounded corners. Icons of the two
52  * member apps are set into these rectangles.
53  */
54 public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
55     private static final String TAG = "AppPairIcon";
56 
57     // A view that holds the app pair icon graphic.
58     private AppPairIconGraphic mIconGraphic;
59     // A view that holds the app pair's title.
60     private BubbleTextView mAppPairName;
61     // The underlying ItemInfo that stores info about the app pair members, etc.
62     private AppPairInfo mInfo;
63     // The containing element that holds this icon: workspace, taskbar, folder, etc. Affects certain
64     // aspects of how the icon is drawn.
65     private int mContainer;
66 
67     // Required for Reorderable -- handles translation and bouncing movements
68     private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
69     private float mScaleForReorderBounce = 1f;
70 
AppPairIcon(Context context, AttributeSet attrs)71     public AppPairIcon(Context context, AttributeSet attrs) {
72         super(context, attrs);
73     }
74 
AppPairIcon(Context context)75     public AppPairIcon(Context context) {
76         super(context);
77     }
78 
79     /**
80      * Builds an AppPairIcon to be added to the Launcher.
81      */
inflateIcon(int resId, ActivityContext activity, @Nullable ViewGroup group, AppPairInfo appPairInfo, int container)82     public static AppPairIcon inflateIcon(int resId, ActivityContext activity,
83             @Nullable ViewGroup group, AppPairInfo appPairInfo, int container) {
84         DeviceProfile grid = activity.getDeviceProfile();
85         LayoutInflater inflater = (group != null)
86                 ? LayoutInflater.from(group.getContext())
87                 : activity.getLayoutInflater();
88         AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false);
89 
90         if (Flags.enableFocusOutline() && activity instanceof Launcher) {
91             icon.setOnFocusChangeListener(((Launcher) activity).getFocusHandler());
92             icon.setDefaultFocusHighlightEnabled(false);
93         }
94 
95         // Sort contents, so that left-hand app comes first
96         appPairInfo.getContents().sort(Comparator.comparingInt(a -> a.rank));
97 
98         icon.setTag(appPairInfo);
99         icon.setOnClickListener(activity.getItemOnClickListener());
100         icon.mInfo = appPairInfo;
101         icon.mContainer = container;
102 
103         // Set up icon drawable area
104         icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic);
105         icon.mIconGraphic.init(icon, container);
106 
107         // Set up app pair title
108         icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
109         FrameLayout.LayoutParams lp =
110                 (FrameLayout.LayoutParams) icon.mAppPairName.getLayoutParams();
111         // Shift the title text down to leave room for the icon graphic. Since the icon graphic is
112         // a separate element (and not set as a CompoundDrawable on the BubbleTextView), we need to
113         // shift the text down manually.
114         lp.topMargin = container == DISPLAY_FOLDER
115                 ? grid.folderChildIconSizePx + grid.folderChildDrawablePaddingPx
116                 : grid.iconSizePx + grid.iconDrawablePaddingPx;
117         // For some reason, app icons have setIncludeFontPadding(false) inside folders, so we set it
118         // here to match that.
119         icon.mAppPairName.setIncludeFontPadding(container != DISPLAY_FOLDER);
120         // Set title text and accessibility title text.
121         icon.updateTitleAndA11yTitle();
122 
123         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
124 
125         return icon;
126     }
127 
128     /**
129      * Updates the title and a11y title of the app pair. Called on creation and when packages
130      * change, to reflect app name changes or user language changes.
131      */
updateTitleAndA11yTitle()132     public void updateTitleAndA11yTitle() {
133         updateTitleAndTextView();
134         updateAccessibilityTitle();
135     }
136 
137     /**
138      * Updates AppPairInfo with a formatted app pair title, and sets it on the BubbleTextView.
139      */
updateTitleAndTextView()140     public void updateTitleAndTextView() {
141         CharSequence newTitle = getInfo().generateTitle(getContext());
142         mAppPairName.setText(newTitle);
143     }
144 
145     /**
146      * Updates the accessibility title with a formatted string template.
147      */
updateAccessibilityTitle()148     public void updateAccessibilityTitle() {
149         CharSequence app1 = getInfo().getFirstApp().title;
150         CharSequence app2 = getInfo().getSecondApp().title;
151         String a11yTitle = getContext().getString(R.string.app_pair_name_format, app1, app2);
152         setContentDescription(
153                 getInfo().shouldReportDisabled(getContext())
154                         ? getContext().getString(R.string.disabled_app_label, a11yTitle)
155                         : a11yTitle);
156     }
157 
158     // Required for DraggableView
159     @Override
getViewType()160     public int getViewType() {
161         return DRAGGABLE_ICON;
162     }
163 
164     // Required for DraggableView
165     @Override
getWorkspaceVisualDragBounds(Rect outBounds)166     public void getWorkspaceVisualDragBounds(Rect outBounds) {
167         mIconGraphic.getIconBounds(outBounds);
168     }
169 
170     /** Sets the visibility of the icon's title text */
setTextVisible(boolean visible)171     public void setTextVisible(boolean visible) {
172         if (visible) {
173             mAppPairName.setVisibility(VISIBLE);
174         } else {
175             mAppPairName.setVisibility(INVISIBLE);
176         }
177     }
178 
179     // Required for Reorderable
180     @Override
getTranslateDelegate()181     public MultiTranslateDelegate getTranslateDelegate() {
182         return mTranslateDelegate;
183     }
184 
185     // Required for Reorderable
186     @Override
setReorderBounceScale(float scale)187     public void setReorderBounceScale(float scale) {
188         mScaleForReorderBounce = scale;
189         super.setScaleX(scale);
190         super.setScaleY(scale);
191     }
192 
193     // Required for Reorderable
194     @Override
getReorderBounceScale()195     public float getReorderBounceScale() {
196         return mScaleForReorderBounce;
197     }
198 
getInfo()199     public AppPairInfo getInfo() {
200         return mInfo;
201     }
202 
getTitleTextView()203     public BubbleTextView getTitleTextView() {
204         return mAppPairName;
205     }
206 
getIconDrawableArea()207     public AppPairIconGraphic getIconDrawableArea() {
208         return mIconGraphic;
209     }
210 
getContainer()211     public int getContainer() {
212         return mContainer;
213     }
214 
215     /**
216      * Ensures that both app icons in the pair are loaded in high resolution.
217      */
verifyHighRes()218     public void verifyHighRes() {
219         IconCache iconCache = LauncherAppState.getInstance(getContext()).getIconCache();
220         getInfo().fetchHiResIconsIfNeeded(iconCache);
221     }
222 
223     /**
224      * Called when WorkspaceItemInfos get updated, and the app pair icon may need to be redrawn.
225      */
maybeRedrawForWorkspaceUpdate(Predicate<ItemInfo> itemCheck)226     public void maybeRedrawForWorkspaceUpdate(Predicate<ItemInfo> itemCheck) {
227         // If either of the app pair icons return true on the predicate (i.e. in the list of
228         // updated apps), redraw the icon graphic (icon background and both icons).
229         if (getInfo().anyMatch(itemCheck)) {
230             updateTitleAndA11yTitle();
231             mIconGraphic.redraw();
232         }
233     }
234 
235     /**
236      * Inside folders, icons are vertically centered in their rows. See
237      * {@link BubbleTextView} for comparison.
238      */
239     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)240     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
241         if (mContainer == DISPLAY_FOLDER) {
242             int height = MeasureSpec.getSize(heightMeasureSpec);
243             ActivityContext activity = ActivityContext.lookupContext(getContext());
244             Paint.FontMetrics fm = mAppPairName.getPaint().getFontMetrics();
245             int cellHeightPx = activity.getDeviceProfile().folderChildIconSizePx
246                     + activity.getDeviceProfile().folderChildDrawablePaddingPx
247                     + (int) Math.ceil(fm.bottom - fm.top);
248             setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(),
249                     getPaddingBottom());
250         }
251         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
252     }
253 }
254