1 /*
2  * Copyright (C) 2008 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;
18 
19 import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
20 
21 import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
22 import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
23 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
24 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
25 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
26 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
27 
28 import android.annotation.SuppressLint;
29 import android.app.ActivityManager;
30 import android.app.ActivityOptions;
31 import android.app.Person;
32 import android.app.WallpaperManager;
33 import android.content.Context;
34 import android.content.pm.LauncherActivityInfo;
35 import android.content.pm.LauncherApps;
36 import android.content.pm.ShortcutInfo;
37 import android.content.res.Configuration;
38 import android.content.res.Resources;
39 import android.graphics.Bitmap;
40 import android.graphics.BlendMode;
41 import android.graphics.BlendModeColorFilter;
42 import android.graphics.Color;
43 import android.graphics.ColorFilter;
44 import android.graphics.LightingColorFilter;
45 import android.graphics.Matrix;
46 import android.graphics.Paint;
47 import android.graphics.Point;
48 import android.graphics.PointF;
49 import android.graphics.Rect;
50 import android.graphics.RectF;
51 import android.graphics.drawable.AdaptiveIconDrawable;
52 import android.graphics.drawable.BitmapDrawable;
53 import android.graphics.drawable.ColorDrawable;
54 import android.graphics.drawable.Drawable;
55 import android.graphics.drawable.InsetDrawable;
56 import android.os.Build;
57 import android.os.Build.VERSION_CODES;
58 import android.os.DeadObjectException;
59 import android.os.Handler;
60 import android.os.Message;
61 import android.os.TransactionTooLargeException;
62 import android.text.Spannable;
63 import android.text.SpannableString;
64 import android.text.TextUtils;
65 import android.text.style.TtsSpan;
66 import android.util.DisplayMetrics;
67 import android.util.Log;
68 import android.util.Pair;
69 import android.util.TypedValue;
70 import android.view.MotionEvent;
71 import android.view.View;
72 import android.view.ViewConfiguration;
73 import android.view.ViewGroup;
74 import android.view.animation.Interpolator;
75 
76 import androidx.annotation.ChecksSdkIntAtLeast;
77 import androidx.annotation.IntDef;
78 import androidx.annotation.NonNull;
79 import androidx.annotation.Nullable;
80 import androidx.annotation.WorkerThread;
81 import androidx.core.graphics.ColorUtils;
82 
83 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
84 import com.android.launcher3.graphics.TintedDrawableSpan;
85 import com.android.launcher3.icons.BitmapInfo;
86 import com.android.launcher3.icons.LauncherIcons;
87 import com.android.launcher3.icons.ShortcutCachingLogic;
88 import com.android.launcher3.icons.ThemedIconDrawable;
89 import com.android.launcher3.model.data.ItemInfo;
90 import com.android.launcher3.model.data.ItemInfoWithIcon;
91 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
92 import com.android.launcher3.pm.UserCache;
93 import com.android.launcher3.shortcuts.ShortcutKey;
94 import com.android.launcher3.shortcuts.ShortcutRequest;
95 import com.android.launcher3.testing.shared.ResourceUtils;
96 import com.android.launcher3.util.FlagOp;
97 import com.android.launcher3.util.IntArray;
98 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
99 import com.android.launcher3.util.Themes;
100 import com.android.launcher3.views.ActivityContext;
101 import com.android.launcher3.views.BaseDragLayer;
102 import com.android.launcher3.widget.PendingAddShortcutInfo;
103 
104 import java.lang.reflect.Method;
105 import java.util.Collections;
106 import java.util.List;
107 import java.util.Locale;
108 import java.util.function.Predicate;
109 import java.util.regex.Matcher;
110 import java.util.regex.Pattern;
111 
112 /**
113  * Various utilities shared amongst the Launcher's classes.
114  */
115 public final class Utilities {
116 
117     private static final String TAG = "Launcher.Utilities";
118 
119     private static final Pattern sTrimPattern =
120             Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$");
121 
122     private static final Matrix sMatrix = new Matrix();
123     private static final Matrix sInverseMatrix = new Matrix();
124 
125     public static final String[] EMPTY_STRING_ARRAY = new String[0];
126     public static final Person[] EMPTY_PERSON_ARRAY = new Person[0];
127 
128     @ChecksSdkIntAtLeast(api = VERSION_CODES.S)
129     public static final boolean ATLEAST_S = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
130 
131     @ChecksSdkIntAtLeast(api = VERSION_CODES.TIRAMISU, codename = "T")
132     public static final boolean ATLEAST_T = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
133 
134     @ChecksSdkIntAtLeast(api = VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "U")
135     public static final boolean ATLEAST_U = Build.VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE;
136 
137     @ChecksSdkIntAtLeast(api = VERSION_CODES.VANILLA_ICE_CREAM, codename = "V")
138     public static final boolean ATLEAST_V = Build.VERSION.SDK_INT
139             >= VERSION_CODES.VANILLA_ICE_CREAM;
140 
141     /**
142      * Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}.
143      */
144     public static final int EDGE_NAV_BAR = 1 << 8;
145 
146     /**
147      * Indicates if the device has a debug build. Should only be used to store additional info or
148      * add extra logging and not for changing the app behavior.
149      * @deprecated Use {@link BuildConfig#IS_DEBUG_DEVICE} directly
150      */
151     @Deprecated
152     public static final boolean IS_DEBUG_DEVICE = BuildConfig.IS_DEBUG_DEVICE;
153 
154     public static final int TRANSLATE_UP = 0;
155     public static final int TRANSLATE_DOWN = 1;
156     public static final int TRANSLATE_LEFT = 2;
157     public static final int TRANSLATE_RIGHT = 3;
158 
159     public static final boolean SHOULD_SHOW_FIRST_PAGE_WIDGET =
160             enableSmartspaceAsAWidget() && WIDGET_ON_FIRST_SCREEN;
161 
162     @IntDef({TRANSLATE_UP, TRANSLATE_DOWN, TRANSLATE_LEFT, TRANSLATE_RIGHT})
163     public @interface AdjustmentDirection{}
164 
165     /**
166      * Returns true if theme is dark.
167      */
isDarkTheme(Context context)168     public static boolean isDarkTheme(Context context) {
169         Configuration configuration = context.getResources().getConfiguration();
170         int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
171         return nightMode == Configuration.UI_MODE_NIGHT_YES;
172     }
173 
174     private static boolean sIsRunningInTestHarness = ActivityManager.isRunningInTestHarness();
175 
isRunningInTestHarness()176     public static boolean isRunningInTestHarness() {
177         return sIsRunningInTestHarness;
178     }
179 
enableRunningInTestHarnessForTests()180     public static void enableRunningInTestHarnessForTests() {
181         sIsRunningInTestHarness = true;
182     }
183 
isPropertyEnabled(String propertyName)184     public static boolean isPropertyEnabled(String propertyName) {
185         return Log.isLoggable(propertyName, Log.VERBOSE);
186     }
187 
188     /**
189      * Given a coordinate relative to the descendant, find the coordinate in a parent view's
190      * coordinates.
191      *
192      * @param descendant The descendant to which the passed coordinate is relative.
193      * @param ancestor The root view to make the coordinates relative to.
194      * @param coord The coordinate that we want mapped.
195      * @param includeRootScroll Whether or not to account for the scroll of the descendant:
196      *          sometimes this is relevant as in a child's coordinates within the descendant.
197      * @return The factor by which this descendant is scaled relative to this DragLayer. Caution
198      *         this scale factor is assumed to be equal in X and Y, and so if at any point this
199      *         assumption fails, we will need to return a pair of scale factors.
200      */
getDescendantCoordRelativeToAncestor( View descendant, View ancestor, float[] coord, boolean includeRootScroll)201     public static float getDescendantCoordRelativeToAncestor(
202             View descendant, View ancestor, float[] coord, boolean includeRootScroll) {
203         return getDescendantCoordRelativeToAncestor(descendant, ancestor, coord, includeRootScroll,
204                 false);
205     }
206 
207     /**
208      * Given a coordinate relative to the descendant, find the coordinate in a parent view's
209      * coordinates.
210      *
211      * @param descendant The descendant to which the passed coordinate is relative.
212      * @param ancestor The root view to make the coordinates relative to.
213      * @param coord The coordinate that we want mapped.
214      * @param includeRootScroll Whether or not to account for the scroll of the descendant:
215      *          sometimes this is relevant as in a child's coordinates within the descendant.
216      * @param ignoreTransform If true, view transform is ignored
217      * @return The factor by which this descendant is scaled relative to this DragLayer. Caution
218      *         this scale factor is assumed to be equal in X and Y, and so if at any point this
219      *         assumption fails, we will need to return a pair of scale factors.
220      */
getDescendantCoordRelativeToAncestor(View descendant, View ancestor, float[] coord, boolean includeRootScroll, boolean ignoreTransform)221     public static float getDescendantCoordRelativeToAncestor(View descendant, View ancestor,
222             float[] coord, boolean includeRootScroll, boolean ignoreTransform) {
223         float scale = 1.0f;
224         View v = descendant;
225         while(v != ancestor && v != null) {
226             // For TextViews, scroll has a meaning which relates to the text position
227             // which is very strange... ignore the scroll.
228             if (v != descendant || includeRootScroll) {
229                 offsetPoints(coord, -v.getScrollX(), -v.getScrollY());
230             }
231 
232             if (!ignoreTransform) {
233                 v.getMatrix().mapPoints(coord);
234             }
235             offsetPoints(coord, v.getLeft(), v.getTop());
236             scale *= v.getScaleX();
237 
238             v = v.getParent() instanceof View ? (View) v.getParent() : null;
239         }
240         return scale;
241     }
242 
243     /**
244      * Returns bounds for a child view of DragLayer, in drag layer coordinates.
245      *
246      * see {@link com.android.launcher3.dragndrop.DragLayer}.
247      *
248      * @param viewBounds Bounds of the view wanted in drag layer coordinates, relative to the view
249      *                   itself. eg. (0, 0, view.getWidth, view.getHeight)
250      * @param ignoreTransform If true, view transform is ignored
251      * @param outRect The out rect where we return the bounds of {@param view} in drag layer coords.
252      */
getBoundsForViewInDragLayer(BaseDragLayer dragLayer, View view, Rect viewBounds, boolean ignoreTransform, float[] recycle, RectF outRect)253     public static void getBoundsForViewInDragLayer(BaseDragLayer dragLayer, View view,
254             Rect viewBounds, boolean ignoreTransform, float[] recycle, RectF outRect) {
255         float[] points = recycle == null ? new float[4] : recycle;
256         points[0] = viewBounds.left;
257         points[1] = viewBounds.top;
258         points[2] = viewBounds.right;
259         points[3] = viewBounds.bottom;
260 
261         Utilities.getDescendantCoordRelativeToAncestor(view, dragLayer, points,
262                 false, ignoreTransform);
263         outRect.set(
264                 Math.min(points[0], points[2]),
265                 Math.min(points[1], points[3]),
266                 Math.max(points[0], points[2]),
267                 Math.max(points[1], points[3]));
268     }
269 
270     /**
271      * Similar to {@link #mapCoordInSelfToDescendant(View descendant, View root, float[] coord)}
272      * but accepts a Rect instead of float[].
273      */
mapRectInSelfToDescendant(View descendant, View root, Rect rect)274     public static void mapRectInSelfToDescendant(View descendant, View root, Rect rect) {
275         float[] coords = new float[]{rect.left, rect.top, rect.right, rect.bottom};
276         mapCoordInSelfToDescendant(descendant, root, coords);
277         rect.set((int) coords[0], (int) coords[1], (int) coords[2], (int) coords[3]);
278     }
279 
280     /**
281      * Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, float[], boolean)}.
282      */
mapCoordInSelfToDescendant(View descendant, View root, float[] coord)283     public static void mapCoordInSelfToDescendant(View descendant, View root, float[] coord) {
284         sMatrix.reset();
285         View v = descendant;
286         while(v != root) {
287             sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY());
288             sMatrix.postConcat(v.getMatrix());
289             sMatrix.postTranslate(v.getLeft(), v.getTop());
290             v = (View) v.getParent();
291         }
292         sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY());
293         sMatrix.invert(sInverseMatrix);
294         sInverseMatrix.mapPoints(coord);
295     }
296 
297     /**
298      * Sets {@param out} to be same as {@param in} by rounding individual values
299      */
roundArray(float[] in, int[] out)300     public static void roundArray(float[] in, int[] out) {
301         for (int i = 0; i < in.length; i++) {
302             out[i] = Math.round(in[i]);
303         }
304     }
305 
offsetPoints(float[] points, float offsetX, float offsetY)306     public static void offsetPoints(float[] points, float offsetX, float offsetY) {
307         for (int i = 0; i < points.length; i += 2) {
308             points[i] += offsetX;
309             points[i + 1] += offsetY;
310         }
311     }
312 
313     /**
314      * Utility method to determine whether the given point, in local coordinates,
315      * is inside the view, where the area of the view is expanded by the slop factor.
316      * This method is called while processing touch-move events to determine if the event
317      * is still within the view.
318      */
pointInView(View v, float localX, float localY, float slop)319     public static boolean pointInView(View v, float localX, float localY, float slop) {
320         return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) &&
321                 localY < (v.getHeight() + slop);
322     }
323 
scaleRectFAboutCenter(RectF r, float scale)324     public static void scaleRectFAboutCenter(RectF r, float scale) {
325         scaleRectFAboutCenter(r, scale, scale);
326     }
327 
328     /**
329      * Similar to {@link #scaleRectAboutCenter(Rect, float)} except this allows different scales
330      * for X and Y
331      */
scaleRectFAboutCenter(RectF r, float scaleX, float scaleY)332     public static void scaleRectFAboutCenter(RectF r, float scaleX, float scaleY) {
333         float px = r.centerX();
334         float py = r.centerY();
335         r.offset(-px, -py);
336         r.left = r.left * scaleX;
337         r.top = r.top * scaleY;
338         r.right = r.right * scaleX;
339         r.bottom = r.bottom * scaleY;
340         r.offset(px, py);
341     }
342 
scaleRectAboutCenter(Rect r, float scale)343     public static void scaleRectAboutCenter(Rect r, float scale) {
344         if (scale != 1.0f) {
345             float cx = r.exactCenterX();
346             float cy = r.exactCenterY();
347             r.left = Math.round(cx + (r.left - cx) * scale);
348             r.top = Math.round(cy + (r.top - cy) * scale);
349             r.right = Math.round(cx + (r.right - cx) * scale);
350             r.bottom = Math.round(cy + (r.bottom - cy) * scale);
351         }
352     }
353 
shrinkRect(Rect r, float scaleX, float scaleY)354     public static float shrinkRect(Rect r, float scaleX, float scaleY) {
355         float scale = Math.min(Math.min(scaleX, scaleY), 1.0f);
356         if (scale < 1.0f) {
357             int deltaX = (int) (r.width() * (scaleX - scale) * 0.5f);
358             r.left += deltaX;
359             r.right -= deltaX;
360 
361             int deltaY = (int) (r.height() * (scaleY - scale) * 0.5f);
362             r.top += deltaY;
363             r.bottom -= deltaY;
364         }
365         return scale;
366     }
367 
368     /**
369      * Sets the x and y pivots for scaling from one Rect to another.
370      *
371      * @param src the source rectangle to scale from.
372      * @param dst the destination rectangle to scale to.
373      * @param outPivot the pivots set for scaling from src to dst.
374      */
getPivotsForScalingRectToRect(Rect src, Rect dst, PointF outPivot)375     public static void getPivotsForScalingRectToRect(Rect src, Rect dst, PointF outPivot) {
376         float pivotXPct = ((float) src.left - dst.left) / ((float) dst.width() - src.width());
377         outPivot.x = dst.left + dst.width() * pivotXPct;
378 
379         float pivotYPct = ((float) src.top - dst.top) / ((float) dst.height() - src.height());
380         outPivot.y = dst.top + dst.height() * pivotYPct;
381     }
382 
383     /**
384      * Maps t from one range to another range.
385      * @param t The value to map.
386      * @param fromMin The lower bound of the range that t is being mapped from.
387      * @param fromMax The upper bound of the range that t is being mapped from.
388      * @param toMin The lower bound of the range that t is being mapped to.
389      * @param toMax The upper bound of the range that t is being mapped to.
390      * @return The mapped value of t.
391      */
mapToRange(float t, float fromMin, float fromMax, float toMin, float toMax, Interpolator interpolator)392     public static float mapToRange(float t, float fromMin, float fromMax, float toMin, float toMax,
393             Interpolator interpolator) {
394         if (fromMin == fromMax || toMin == toMax) {
395             Log.e(TAG, "mapToRange: range has 0 length");
396             return toMin;
397         }
398         float progress = getProgress(t, fromMin, fromMax);
399         return mapRange(interpolator.getInterpolation(progress), toMin, toMax);
400     }
401 
402     /** Bounds t between a lower and upper bound and maps the result to a range. */
mapBoundToRange(float t, float lowerBound, float upperBound, float toMin, float toMax, Interpolator interpolator)403     public static float mapBoundToRange(float t, float lowerBound, float upperBound,
404             float toMin, float toMax, Interpolator interpolator) {
405         return mapToRange(boundToRange(t, lowerBound, upperBound), lowerBound, upperBound,
406                 toMin, toMax, interpolator);
407     }
408 
getProgress(float current, float min, float max)409     public static float getProgress(float current, float min, float max) {
410         return Math.abs(current - min) / Math.abs(max - min);
411     }
412 
mapRange(float value, float min, float max)413     public static float mapRange(float value, float min, float max) {
414         return min + (value * (max - min));
415     }
416 
417     /**
418      * Trims the string, removing all whitespace at the beginning and end of the string.
419      * Non-breaking whitespaces are also removed.
420      */
421     @NonNull
trim(CharSequence s)422     public static String trim(CharSequence s) {
423         if (s == null) {
424             return "";
425         }
426 
427         // Just strip any sequence of whitespace or java space characters from the beginning and end
428         Matcher m = sTrimPattern.matcher(s);
429         return m.replaceAll("$1");
430     }
431 
432     /**
433      * Calculates the height of a given string at a specific text size.
434      */
calculateTextHeight(float textSizePx)435     public static int calculateTextHeight(float textSizePx) {
436         Paint p = new Paint();
437         p.setTextSize(textSizePx);
438         Paint.FontMetrics fm = p.getFontMetrics();
439         return (int) Math.ceil(fm.bottom - fm.top);
440     }
441 
isRtl(Resources res)442     public static boolean isRtl(Resources res) {
443         return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
444     }
445 
446     /** Converts a pixel value (px) to scale pixel value (SP) for the current device. */
pxToSp(float size)447     public static float pxToSp(float size) {
448         return size / Resources.getSystem().getDisplayMetrics().scaledDensity;
449     }
450 
dpiFromPx(float size, int densityDpi)451     public static float dpiFromPx(float size, int densityDpi) {
452         float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
453         return (size / densityRatio);
454     }
455 
456     /** Converts a dp value to pixels for the current device. */
dpToPx(float dp)457     public static int dpToPx(float dp) {
458         return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
459     }
460 
461     /** Converts a dp value to pixels for a certain density. */
dpToPx(float dp, int densityDpi)462     public static int dpToPx(float dp, int densityDpi) {
463         float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
464         return (int) (dp * densityRatio);
465     }
466 
pxFromSp(float size, DisplayMetrics metrics)467     public static int pxFromSp(float size, DisplayMetrics metrics) {
468         return pxFromSp(size, metrics, 1f);
469     }
470 
pxFromSp(float size, DisplayMetrics metrics, float scale)471     public static int pxFromSp(float size, DisplayMetrics metrics, float scale) {
472         float value = scale * TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, size, metrics);
473         return ResourceUtils.roundPxValueFromFloat(value);
474     }
475 
createDbSelectionQuery(String columnName, IntArray values)476     public static String createDbSelectionQuery(String columnName, IntArray values) {
477         return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, values.toConcatString());
478     }
479 
isBootCompleted()480     public static boolean isBootCompleted() {
481         return "1".equals(getSystemProperty("sys.boot_completed", "1"));
482     }
483 
getSystemProperty(String property, String defaultValue)484     public static String getSystemProperty(String property, String defaultValue) {
485         try {
486             Class clazz = Class.forName("android.os.SystemProperties");
487             Method getter = clazz.getDeclaredMethod("get", String.class);
488             String value = (String) getter.invoke(null, property);
489             if (!TextUtils.isEmpty(value)) {
490                 return value;
491             }
492         } catch (Exception e) {
493             Log.d(TAG, "Unable to read system properties");
494         }
495         return defaultValue;
496     }
497 
498     /**
499      * Ensures that a value is within given bounds. Specifically:
500      * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound,
501      * return upperBound; else return value unchanged.
502      */
boundToRange(int value, int lowerBound, int upperBound)503     public static int boundToRange(int value, int lowerBound, int upperBound) {
504         return Math.max(lowerBound, Math.min(value, upperBound));
505     }
506 
507     /**
508      * @see #boundToRange(int, int, int).
509      */
boundToRange(float value, float lowerBound, float upperBound)510     public static float boundToRange(float value, float lowerBound, float upperBound) {
511         return Math.max(lowerBound, Math.min(value, upperBound));
512     }
513 
514     /**
515      * @see #boundToRange(int, int, int).
516      */
boundToRange(long value, long lowerBound, long upperBound)517     public static long boundToRange(long value, long lowerBound, long upperBound) {
518         return Math.max(lowerBound, Math.min(value, upperBound));
519     }
520 
521     /**
522      * Wraps a message with a TTS span, so that a different message is spoken than
523      * what is getting displayed.
524      * @param msg original message
525      * @param ttsMsg message to be spoken
526      */
wrapForTts(CharSequence msg, String ttsMsg)527     public static CharSequence wrapForTts(CharSequence msg, String ttsMsg) {
528         SpannableString spanned = new SpannableString(msg);
529         spanned.setSpan(new TtsSpan.TextBuilder(ttsMsg).build(),
530                 0, spanned.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
531         return spanned;
532     }
533 
534     /**
535      * Prefixes a text with the provided icon
536      */
prefixTextWithIcon(Context context, int iconRes, CharSequence msg)537     public static CharSequence prefixTextWithIcon(Context context, int iconRes, CharSequence msg) {
538         // Update the hint to contain the icon.
539         // Prefix the original hint with two spaces. The first space gets replaced by the icon
540         // using span. The second space is used for a singe space character between the hint
541         // and the icon.
542         SpannableString spanned = new SpannableString("  " + msg);
543         spanned.setSpan(new TintedDrawableSpan(context, iconRes),
544                 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
545         return spanned;
546     }
547 
isWallpaperSupported(Context context)548     public static boolean isWallpaperSupported(Context context) {
549         return context.getSystemService(WallpaperManager.class).isWallpaperSupported();
550     }
551 
isWallpaperAllowed(Context context)552     public static boolean isWallpaperAllowed(Context context) {
553         return context.getSystemService(WallpaperManager.class).isSetWallpaperAllowed();
554     }
555 
isBinderSizeError(Exception e)556     public static boolean isBinderSizeError(Exception e) {
557         return e.getCause() instanceof TransactionTooLargeException
558                 || e.getCause() instanceof DeadObjectException;
559     }
560 
561     /**
562      * Utility method to post a runnable on the handler, skipping the synchronization barriers.
563      */
postAsyncCallback(Handler handler, Runnable callback)564     public static void postAsyncCallback(Handler handler, Runnable callback) {
565         Message msg = Message.obtain(handler, callback);
566         msg.setAsynchronous(true);
567         handler.sendMessage(msg);
568     }
569 
570     /**
571      * Utility method to allow background activity launch for the provided activity options
572      */
allowBGLaunch(ActivityOptions options)573     public static ActivityOptions allowBGLaunch(ActivityOptions options) {
574         if (ATLEAST_U) {
575             options.setPendingIntentBackgroundActivityStartMode(
576                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
577         }
578         return options;
579     }
580 
581     /**
582      * Returns the full drawable for info as multiple layers of AdaptiveIconDrawable. The second
583      * drawable in the Pair is the badge used with the icon.
584      *
585      * @param useTheme If true, will theme icons when applicable
586      */
587     @SuppressLint("UseCompatLoadingForDrawables")
588     @Nullable
589     @WorkerThread
590     public static <T extends Context & ActivityContext> Pair<AdaptiveIconDrawable, Drawable>
getFullDrawable(T context, ItemInfo info, int width, int height, boolean useTheme)591             getFullDrawable(T context, ItemInfo info, int width, int height, boolean useTheme) {
592         useTheme &= Themes.isThemedIconEnabled(context);
593         LauncherAppState appState = LauncherAppState.getInstance(context);
594         Drawable mainIcon = null;
595 
596         Drawable badge = null;
597         if ((info instanceof ItemInfoWithIcon iiwi) && !iiwi.usingLowResIcon()) {
598             badge = iiwi.bitmap.getBadgeDrawable(context, useTheme);
599         }
600 
601         if (info instanceof PendingAddShortcutInfo) {
602             ShortcutConfigActivityInfo activityInfo =
603                     ((PendingAddShortcutInfo) info).getActivityInfo(context);
604             mainIcon = activityInfo.getFullResIcon(appState.getIconCache());
605         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
606             LauncherActivityInfo activityInfo = context.getSystemService(LauncherApps.class)
607                     .resolveActivity(info.getIntent(), info.user);
608             if (activityInfo == null) {
609                 return null;
610             }
611             mainIcon = appState.getIconProvider().getIcon(
612                     activityInfo, appState.getInvariantDeviceProfile().fillResIconDpi);
613         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
614             List<ShortcutInfo> siList = ShortcutKey.fromItemInfo(info)
615                     .buildRequest(context)
616                     .query(ShortcutRequest.ALL);
617             if (siList.isEmpty()) {
618                 return null;
619             } else {
620                 ShortcutInfo si = siList.get(0);
621                 mainIcon = ShortcutCachingLogic.getIcon(context, si,
622                         appState.getInvariantDeviceProfile().fillResIconDpi);
623                 // Only fetch badge if the icon is on workspace
624                 if (info.id != ItemInfo.NO_ID && badge == null) {
625                     badge = appState.getIconCache().getShortcutInfoBadge(si)
626                             .newIcon(context, FLAG_THEMED);
627                 }
628             }
629         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
630             FolderAdaptiveIcon icon = FolderAdaptiveIcon.createFolderAdaptiveIcon(
631                     context, info.id, new Point(width, height));
632             if (icon == null) {
633                 return null;
634             }
635             mainIcon =  icon;
636             badge = icon.getBadge();
637         }
638 
639         if (mainIcon == null) {
640             return null;
641         }
642         AdaptiveIconDrawable result;
643         if (mainIcon instanceof AdaptiveIconDrawable aid) {
644             result = aid;
645         } else {
646             // Wrap the main icon in AID
647             try (LauncherIcons li = LauncherIcons.obtain(context)) {
648                 result = li.wrapToAdaptiveIcon(mainIcon, null);
649             }
650         }
651         if (result == null) {
652             return null;
653         }
654 
655         // Inject monochrome icon drawable
656         if (ATLEAST_T && useTheme) {
657             result.mutate();
658             int[] colors = ThemedIconDrawable.getColors(context);
659             Drawable mono = result.getMonochrome();
660 
661             if (mono != null) {
662                 mono.setTint(colors[1]);
663             } else  if (info instanceof ItemInfoWithIcon iiwi) {
664                 // Inject a previously generated monochrome icon
665                 Bitmap monoBitmap = iiwi.bitmap.getMono();
666                 if (monoBitmap != null) {
667                     // Use BitmapDrawable instead of FastBitmapDrawable so that the colorState is
668                     // preserved in constantState
669                     mono = new BitmapDrawable(monoBitmap);
670                     mono.setColorFilter(new BlendModeColorFilter(colors[1], BlendMode.SRC_IN));
671                     // Inset the drawable according to the AdaptiveIconDrawable layers
672                     mono = new InsetDrawable(mono, getExtraInsetFraction() / 2);
673                 }
674             }
675             if (mono != null) {
676                 result = new AdaptiveIconDrawable(new ColorDrawable(colors[0]), mono);
677             }
678         }
679 
680         if (badge == null) {
681             badge = BitmapInfo.LOW_RES_INFO.withFlags(
682                             UserCache.INSTANCE.get(context)
683                                     .getUserInfo(info.user)
684                                     .applyBitmapInfoFlags(FlagOp.NO_OP))
685                     .getBadgeDrawable(context, useTheme);
686             if (badge == null) {
687                 badge = new ColorDrawable(Color.TRANSPARENT);
688             }
689         }
690         return Pair.create(result, badge);
691     }
692 
squaredHypot(float x, float y)693     public static float squaredHypot(float x, float y) {
694         return x * x + y * y;
695     }
696 
squaredTouchSlop(Context context)697     public static float squaredTouchSlop(Context context) {
698         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
699         return slop * slop;
700     }
701 
702     /**
703      * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent
704      * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine
705      * the final bounds.
706      */
rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight, int delta)707     public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight,
708             int delta) {
709         int rdelta = ((delta % 4) + 4) % 4;
710         int origLeft = inOutBounds.left;
711         switch (rdelta) {
712             case 0:
713                 return;
714             case 1:
715                 inOutBounds.left = inOutBounds.top;
716                 inOutBounds.top = parentWidth - inOutBounds.right;
717                 inOutBounds.right = inOutBounds.bottom;
718                 inOutBounds.bottom = parentWidth - origLeft;
719                 return;
720             case 2:
721                 inOutBounds.left = parentWidth - inOutBounds.right;
722                 inOutBounds.right = parentWidth - origLeft;
723                 return;
724             case 3:
725                 inOutBounds.left = parentHeight - inOutBounds.bottom;
726                 inOutBounds.bottom = inOutBounds.right;
727                 inOutBounds.right = parentHeight - inOutBounds.top;
728                 inOutBounds.top = origLeft;
729                 return;
730         }
731     }
732 
733     /**
734      * Make a color filter that blends a color into the destination based on a scalable amout.
735      *
736      * @param color to blend in.
737      * @param tintAmount [0-1] 0 no tinting, 1 full color.
738      * @return ColorFilter for tinting, or {@code null} if no filter is needed.
739      */
makeColorTintingColorFilter(int color, float tintAmount)740     public static ColorFilter makeColorTintingColorFilter(int color, float tintAmount) {
741         if (tintAmount == 0f) {
742             return null;
743         }
744         return new LightingColorFilter(
745                 // This isn't blending in white, its making a multiplication mask for the base color
746                 ColorUtils.blendARGB(Color.WHITE, 0, tintAmount),
747                 ColorUtils.blendARGB(0, color, tintAmount));
748     }
749 
getViewBounds(@onNull View v)750     public static Rect getViewBounds(@NonNull View v) {
751         int[] pos = new int[2];
752         v.getLocationOnScreen(pos);
753         return new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight());
754     }
755 
756     /**
757      * Returns a list of screen-splitting options depending on the device orientation (split top for
758      * portrait, split right for landscape)
759      */
getSplitPositionOptions( DeviceProfile dp)760     public static List<SplitPositionOption> getSplitPositionOptions(
761             DeviceProfile dp) {
762         int splitIconRes = dp.isLeftRightSplit
763                 ? R.drawable.ic_split_horizontal
764                 : R.drawable.ic_split_vertical;
765         int stagePosition = dp.isLeftRightSplit
766                 ? STAGE_POSITION_BOTTOM_OR_RIGHT
767                 : STAGE_POSITION_TOP_OR_LEFT;
768         return Collections.singletonList(new SplitPositionOption(
769                 splitIconRes,
770                 R.string.recent_task_option_split_screen,
771                 stagePosition,
772                 STAGE_TYPE_MAIN
773         ));
774     }
775 
776     /** Logs the Scale and Translate properties of a matrix. Ignores skew and perspective. */
logMatrix(String label, Matrix matrix)777     public static void logMatrix(String label, Matrix matrix) {
778         float[] matrixValues = new float[9];
779         matrix.getValues(matrixValues);
780         Log.d(label, String.format("%s: %s\nscale (x,y) = (%f, %f)\ntranslate (x,y) = (%f, %f)",
781                 label, matrix, matrixValues[Matrix.MSCALE_X], matrixValues[Matrix.MSCALE_Y],
782                 matrixValues[Matrix.MTRANS_X], matrixValues[Matrix.MTRANS_Y]
783         ));
784     }
785 
786     /**
787      * Translates the {@code targetView} so that it overlaps with {@code exclusionBounds} as little
788      * as possible, while remaining within {@code inclusionBounds}.
789      * <p>
790      * {@code inclusionBounds} will always take precedence over {@code exclusionBounds}, so if
791      * {@code targetView} needs to be translated outside of {@code inclusionBounds} to fully fix an
792      * overlap with {@code exclusionBounds}, then {@code targetView} will only be translated up to
793      * the border of {@code inclusionBounds}.
794      * <p>
795      * Note: {@code targetViewBounds}, {@code inclusionBounds} and {@code exclusionBounds} must all
796      * be in relation to the same reference point on screen.
797      * <p>
798      * @param targetView the view being translated
799      * @param targetViewBounds the bounds of the {@code targetView}
800      * @param inclusionBounds the bounds the {@code targetView} absolutely must stay within
801      * @param exclusionBounds the bounds to try to move the {@code targetView} away from
802      * @param adjustmentDirection the translation direction that should be attempted to fix an
803      *                            overlap
804      */
translateOverlappingView( @onNull View targetView, @NonNull Rect targetViewBounds, @NonNull Rect inclusionBounds, @NonNull Rect exclusionBounds, @AdjustmentDirection int adjustmentDirection)805     public static void translateOverlappingView(
806             @NonNull View targetView,
807             @NonNull Rect targetViewBounds,
808             @NonNull Rect inclusionBounds,
809             @NonNull Rect exclusionBounds,
810             @AdjustmentDirection int adjustmentDirection) {
811         switch (adjustmentDirection) {
812             case TRANSLATE_RIGHT:
813                 targetView.setTranslationX(Math.min(
814                         // Translate to the right if the view is overlapping on the left.
815                         Math.max(0, exclusionBounds.right - targetViewBounds.left),
816                         // Do not translate beyond the inclusion bounds.
817                         inclusionBounds.right - targetViewBounds.right));
818                 break;
819             case TRANSLATE_LEFT:
820                 targetView.setTranslationX(Math.max(
821                         // Translate to the left if the view is overlapping on the right.
822                         Math.min(0, exclusionBounds.left - targetViewBounds.right),
823                         // Do not translate beyond the inclusion bounds.
824                         inclusionBounds.left - targetViewBounds.left));
825                 break;
826             case TRANSLATE_DOWN:
827                 targetView.setTranslationY(Math.min(
828                         // Translate downwards if the view is overlapping on the top.
829                         Math.max(0, exclusionBounds.bottom - targetViewBounds.top),
830                         // Do not translate beyond the inclusion bounds.
831                         inclusionBounds.bottom - targetViewBounds.bottom));
832                 break;
833             case TRANSLATE_UP:
834                 targetView.setTranslationY(Math.max(
835                         // Translate upwards if the view is overlapping on the bottom.
836                         Math.min(0, exclusionBounds.top - targetViewBounds.bottom),
837                         // Do not translate beyond the inclusion bounds.
838                         inclusionBounds.top - targetViewBounds.top));
839                 break;
840             default:
841                 // No-Op
842         }
843     }
844 
845     /**
846      * Does a depth-first search through the View hierarchy starting at root, to find a view that
847      * matches the predicate. Returns null if no View was found. View has a findViewByPredicate
848      * member function but it is currently a @hide API.
849      */
850     @Nullable
findViewByPredicate(@onNull View root, @NonNull Predicate<View> predicate)851     public static <T extends View> T findViewByPredicate(@NonNull View root,
852             @NonNull Predicate<View> predicate) {
853         if (predicate.test(root)) {
854             return (T) root;
855         }
856         if (root instanceof ViewGroup parent) {
857             int count = parent.getChildCount();
858             for (int i = 0; i < count; i++) {
859                 View view = findViewByPredicate(parent.getChildAt(i), predicate);
860                 if (view != null) {
861                     return (T) view;
862                 }
863             }
864         }
865         return null;
866     }
867 }
868