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