1 /*
2  * Copyright (C) 2019 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.uioverrides;
17 
18 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.PIN_PREDICTION;
19 import static com.android.launcher3.graphics.IconShape.getShape;
20 
21 import android.content.Context;
22 import android.graphics.BlurMaskFilter;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Path;
27 import android.graphics.Rect;
28 import android.os.Process;
29 import android.util.AttributeSet;
30 import android.view.LayoutInflater;
31 import android.view.ViewGroup;
32 import android.view.accessibility.AccessibilityNodeInfo;
33 
34 import androidx.core.graphics.ColorUtils;
35 
36 import com.android.launcher3.CellLayout;
37 import com.android.launcher3.DeviceProfile;
38 import com.android.launcher3.Launcher;
39 import com.android.launcher3.R;
40 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
41 import com.android.launcher3.graphics.IconPalette;
42 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
43 import com.android.launcher3.icons.IconNormalizer;
44 import com.android.launcher3.icons.LauncherIcons;
45 import com.android.launcher3.model.data.ItemInfo;
46 import com.android.launcher3.model.data.WorkspaceItemInfo;
47 import com.android.launcher3.touch.ItemClickHandler;
48 import com.android.launcher3.touch.ItemLongClickListener;
49 import com.android.launcher3.util.SafeCloseable;
50 import com.android.launcher3.views.ActivityContext;
51 import com.android.launcher3.views.DoubleShadowBubbleTextView;
52 
53 /**
54  * A BubbleTextView with a ring around it's drawable
55  */
56 public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
57         LauncherAccessibilityDelegate.AccessibilityActionHandler {
58 
59     private static final int RING_SHADOW_COLOR = 0x99000000;
60     private static final float RING_EFFECT_RATIO = 0.095f;
61 
62     boolean mIsDrawingDot = false;
63     private final DeviceProfile mDeviceProfile;
64     private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
65     private final Path mRingPath = new Path();
66     private boolean mIsPinned = false;
67     private final int mNormalizedIconRadius;
68     private final BlurMaskFilter mShadowFilter;
69     private int mPlateColor;
70     boolean mDrawForDrag = false;
71 
PredictedAppIcon(Context context)72     public PredictedAppIcon(Context context) {
73         this(context, null, 0);
74     }
75 
PredictedAppIcon(Context context, AttributeSet attrs)76     public PredictedAppIcon(Context context, AttributeSet attrs) {
77         this(context, attrs, 0);
78     }
79 
PredictedAppIcon(Context context, AttributeSet attrs, int defStyle)80     public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
81         super(context, attrs, defStyle);
82         mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
83         mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
84         int shadowSize = context.getResources().getDimensionPixelSize(
85                 R.dimen.blur_size_thin_outline);
86         mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
87     }
88 
89     @Override
onDraw(Canvas canvas)90     public void onDraw(Canvas canvas) {
91         int count = canvas.save();
92         if (!mIsPinned) {
93             boolean isBadged = getTag() instanceof WorkspaceItemInfo
94                     && !Process.myUserHandle().equals(((ItemInfo) getTag()).user);
95             drawEffect(canvas, isBadged);
96             canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
97             canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
98         }
99         super.onDraw(canvas);
100         canvas.restoreToCount(count);
101     }
102 
103     @Override
drawDotIfNecessary(Canvas canvas)104     protected void drawDotIfNecessary(Canvas canvas) {
105         mIsDrawingDot = true;
106         int count = canvas.save();
107         canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO);
108         canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO);
109         super.drawDotIfNecessary(canvas);
110         canvas.restoreToCount(count);
111         mIsDrawingDot = false;
112     }
113 
114     @Override
applyFromWorkspaceItem(WorkspaceItemInfo info)115     public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
116         super.applyFromWorkspaceItem(info);
117         int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
118         mPlateColor = ColorUtils.setAlphaComponent(color, 200);
119         if (mIsPinned) {
120             setContentDescription(info.contentDescription);
121         } else {
122             setContentDescription(
123                     getContext().getString(R.string.hotseat_prediction_content_description,
124                             info.contentDescription));
125         }
126     }
127 
128     /**
129      * Removes prediction ring from app icon
130      */
pin(WorkspaceItemInfo info)131     public void pin(WorkspaceItemInfo info) {
132         if (mIsPinned) return;
133         mIsPinned = true;
134         applyFromWorkspaceItem(info);
135         setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
136         ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
137         invalidate();
138     }
139 
140     /**
141      * prepares prediction icon for usage after bind
142      */
finishBinding(OnLongClickListener longClickListener)143     public void finishBinding(OnLongClickListener longClickListener) {
144         setOnLongClickListener(longClickListener);
145         ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false;
146         setTextVisibility(false);
147         verifyHighRes();
148     }
149 
150     @Override
addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo)151     public void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo) {
152         if (!mIsPinned) {
153             accessibilityNodeInfo.addAction(
154                     new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION,
155                             getContext().getText(R.string.pin_prediction)));
156         }
157     }
158 
159     @Override
performAccessibilityAction(int action, ItemInfo info)160     public boolean performAccessibilityAction(int action, ItemInfo info) {
161         QuickstepLauncher launcher = Launcher.cast(Launcher.getLauncher(getContext()));
162         if (action == PIN_PREDICTION) {
163             if (launcher == null || launcher.getHotseatPredictionController() == null) {
164                 return false;
165             }
166             HotseatPredictionController controller = launcher.getHotseatPredictionController();
167             controller.pinPrediction(info);
168             return true;
169         }
170         return false;
171     }
172 
173     @Override
getIconBounds(Rect outBounds)174     public void getIconBounds(Rect outBounds) {
175         super.getIconBounds(outBounds);
176         if (!mIsPinned && !mIsDrawingDot) {
177             int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
178             outBounds.inset(predictionInset, predictionInset);
179         }
180     }
181 
getOutlineOffsetX()182     private int getOutlineOffsetX() {
183         return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
184     }
185 
getOutlineOffsetY()186     private int getOutlineOffsetY() {
187         return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
188     }
189 
drawEffect(Canvas canvas, boolean isBadged)190     private void drawEffect(Canvas canvas, boolean isBadged) {
191         // Don't draw ring effect if item is about to be dragged.
192         if (mDrawForDrag) {
193             return;
194         }
195         mRingPath.reset();
196         getShape().addToPath(mRingPath, getOutlineOffsetX(), getOutlineOffsetY(),
197                 mNormalizedIconRadius);
198         if (isBadged) {
199             float outlineSize = mNormalizedIconRadius * RING_EFFECT_RATIO * 2;
200             float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
201             float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
202             float badgeInset = mNormalizedIconRadius * 2 - badgeSize;
203             getShape().addToPath(mRingPath, getOutlineOffsetX() + badgeInset,
204                     getOutlineOffsetY() + badgeInset, badgeSize / 2);
205 
206         }
207         mIconRingPaint.setColor(RING_SHADOW_COLOR);
208         mIconRingPaint.setMaskFilter(mShadowFilter);
209         canvas.drawPath(mRingPath, mIconRingPaint);
210         mIconRingPaint.setColor(mPlateColor);
211         mIconRingPaint.setMaskFilter(null);
212         canvas.drawPath(mRingPath, mIconRingPaint);
213     }
214 
215     @Override
getSourceVisualDragBounds(Rect bounds)216     public void getSourceVisualDragBounds(Rect bounds) {
217         super.getSourceVisualDragBounds(bounds);
218         if (!mIsPinned) {
219             int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO);
220             bounds.inset(internalSize, internalSize);
221         }
222     }
223 
224     @Override
prepareDrawDragView()225     public SafeCloseable prepareDrawDragView() {
226         mDrawForDrag = true;
227         invalidate();
228         SafeCloseable r = super.prepareDrawDragView();
229         return () -> {
230             r.close();
231             mDrawForDrag = false;
232         };
233     }
234 
235     /**
236      * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
237      */
createIcon(ViewGroup parent, WorkspaceItemInfo info)238     public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
239         PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
240                 .inflate(R.layout.predicted_app_icon, parent, false);
241         icon.applyFromWorkspaceItem(info);
242         icon.setOnClickListener(ItemClickHandler.INSTANCE);
243         icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler());
244         return icon;
245     }
246 
247     /**
248      * Draws Predicted Icon outline on cell layout
249      */
250     public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
251 
252         private int mOffsetX;
253         private int mOffsetY;
254         private int mIconRadius;
255         private Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
256 
PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon)257         public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
258             mDelegateCellX = cellX;
259             mDelegateCellY = cellY;
260             mOffsetX = icon.getOutlineOffsetX();
261             mOffsetY = icon.getOutlineOffsetY();
262             mIconRadius = icon.mNormalizedIconRadius;
263             mOutlinePaint.setStyle(Paint.Style.FILL);
264             mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
265         }
266 
267         /**
268          * Draws predicted app icon outline under CellLayout
269          */
270         @Override
drawUnderItem(Canvas canvas)271         public void drawUnderItem(Canvas canvas) {
272             getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
273         }
274 
275         /**
276          * Draws PredictedAppIcon outline over CellLayout
277          */
278         @Override
drawOverItem(Canvas canvas)279         public void drawOverItem(Canvas canvas) {
280             // Does nothing
281         }
282     }
283 }
284