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.car.ui.utils;
17 
18 import static com.android.car.ui.core.CarUi.MIN_TARGET_API;
19 
20 import android.annotation.SuppressLint;
21 import android.annotation.TargetApi;
22 import android.app.Activity;
23 import android.content.Context;
24 import android.content.ContextWrapper;
25 import android.content.res.Resources;
26 import android.content.res.TypedArray;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.drawable.BitmapDrawable;
30 import android.graphics.drawable.Drawable;
31 import android.os.Build;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.util.SparseArray;
35 import android.util.TypedValue;
36 import android.view.View;
37 import android.view.ViewGroup;
38 
39 import androidx.annotation.DimenRes;
40 import androidx.annotation.IdRes;
41 import androidx.annotation.NonNull;
42 import androidx.annotation.Nullable;
43 import androidx.annotation.StyleRes;
44 import androidx.annotation.UiThread;
45 
46 import com.android.car.ui.R;
47 import com.android.car.ui.uxr.DrawableStateView;
48 
49 import java.lang.reflect.Method;
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.function.Function;
53 
54 /**
55  * Collection of utility methods
56  */
57 @SuppressWarnings("AndroidJdkLibsChecker")
58 @TargetApi(MIN_TARGET_API)
59 public final class CarUiUtils {
60 
61     private static final String TAG = "CarUiUtils";
62     private static final String READ_ONLY_SYSTEM_PROPERTY_PREFIX = "ro.";
63     /** A map to cache read-only system properties. */
64     private static final SparseArray<String> READ_ONLY_SYSTEM_PROPERTY_MAP = new SparseArray<>();
65 
66     private static int[] sRestrictedState;
67 
68     /** This is a utility class */
CarUiUtils()69     private CarUiUtils() {
70     }
71 
72     /**
73      * Reads a float value from a dimens resource. This is necessary as {@link Resources#getFloat}
74      * is not currently public.
75      *
76      * @param res   {@link Resources} to read values from
77      * @param resId Id of the dimens resource to read
78      */
getFloat(Resources res, @DimenRes int resId)79     public static float getFloat(Resources res, @DimenRes int resId) {
80         TypedValue outValue = new TypedValue();
81         res.getValue(resId, outValue, true);
82         return outValue.getFloat();
83     }
84 
85     /** Returns the identifier of the resolved resource assigned to the given attribute. */
getAttrResourceId(Context context, int attr)86     public static int getAttrResourceId(Context context, int attr) {
87         return getAttrResourceId(context, /*styleResId=*/ 0, attr);
88     }
89 
90     /**
91      * Returns the identifier of the resolved resource assigned to the given attribute defined in
92      * the given style.
93      */
getAttrResourceId(Context context, @StyleRes int styleResId, int attr)94     public static int getAttrResourceId(Context context, @StyleRes int styleResId, int attr) {
95         TypedArray ta = context.obtainStyledAttributes(styleResId, new int[]{attr});
96         int resId = ta.getResourceId(0, 0);
97         ta.recycle();
98         return resId;
99     }
100 
101     /**
102      * Gets the boolean value of an Attribute from an {@link Activity Activity's}
103      * {@link android.content.res.Resources.Theme}.
104      */
getThemeBoolean(Activity activity, int attr)105     public static boolean getThemeBoolean(Activity activity, int attr) {
106         TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr});
107 
108         try {
109             return a.getBoolean(0, false);
110         } finally {
111             a.recycle();
112         }
113     }
114 
115     /**
116      * Gets the {@link Activity} for a certain {@link Context}.
117      *
118      * <p>It is possible the Context is not associated with an Activity, in which case
119      * this method will return null.
120      */
121     @Nullable
getActivity(@ullable Context context)122     public static Activity getActivity(@Nullable Context context) {
123         while (context instanceof ContextWrapper) {
124             if (context instanceof Activity) {
125                 return (Activity) context;
126             }
127             context = ((ContextWrapper) context).getBaseContext();
128         }
129         return null;
130     }
131 
132     /**
133      * It behaves similarly to {@link View#findViewById(int)}, except that on Q and below,
134      * it will first resolve the id to whatever it references.
135      *
136      * This is to support layout RROs before the new RRO features in R.
137      *
138      * @param id the ID to search for
139      * @return a view with given ID if found, or {@code null} otherwise
140      * @see View#requireViewById(int)
141      */
142     @Nullable
143     @UiThread
144     @SuppressWarnings("TypeParameterUnusedInFormals")
findViewByRefId(@onNull View root, @IdRes int id)145     public static <T extends View> T findViewByRefId(@NonNull View root, @IdRes int id) {
146         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
147             return root.findViewById(id);
148         }
149 
150         if (id == View.NO_ID) {
151             return null;
152         }
153 
154         TypedValue value = new TypedValue();
155         root.getResources().getValue(id, value, true);
156         return root.findViewById(value.resourceId);
157     }
158 
159     /**
160      * It behaves similarly to {@link View#requireViewById(int)}, except that on Q and below,
161      * it will first resolve the id to whatever it references.
162      *
163      * This is to support layout RROs before the new RRO features in R.
164      *
165      * @param id the ID to search for
166      * @return a view with given ID
167      * @see View#findViewById(int)
168      */
169     @NonNull
170     @UiThread
171     @SuppressWarnings("TypeParameterUnusedInFormals")
requireViewByRefId(@onNull View root, @IdRes int id)172     public static <T extends View> T requireViewByRefId(@NonNull View root, @IdRes int id) {
173         T view = findViewByRefId(root, id);
174         if (view == null) {
175             throw new IllegalArgumentException("ID "
176                     + root.getResources().getResourceName(id)
177                     + " does not reference a View inside this View");
178         }
179         return view;
180     }
181 
182     /**
183      * Returns the system property of type boolean. This method converts the boolean value in string
184      * returned by {@link #getSystemProperty(Resources, int)}
185      */
getBooleanSystemProperty( @onNull Resources resources, int propertyResId, boolean defaultValue)186     public static boolean getBooleanSystemProperty(
187             @NonNull Resources resources, int propertyResId, boolean defaultValue) {
188         String value = getSystemProperty(resources, propertyResId);
189 
190         if (!TextUtils.isEmpty(value)) {
191             return Boolean.parseBoolean(value);
192         }
193         return defaultValue;
194     }
195 
196     /**
197      * Use reflection to interact with the hidden API <code>android.os.SystemProperties</code>.
198      *
199      * <p>This method caches read-only properties. CAVEAT: Please do not set read-only properties
200      * by 'adb setprop' after app started. Read-only properties CAN BE SET ONCE if it is unset.
201      * Thus, read-only properties MAY BE CHANGED from unset to set during application's lifetime if
202      * you use 'adb setprop' command to set read-only properties after app started. For the sake of
203      * performance, this method also caches the unset state. Otherwise, cache may not effective if
204      * the system property is unset (which is most-likely).
205      *
206      * @param resources     resources object to fetch string
207      * @param propertyResId the property resource id.
208      * @return The value of the property if defined, else null. Does not return empty strings.
209      */
210     @Nullable
getSystemProperty(@onNull Resources resources, int propertyResId)211     public static String getSystemProperty(@NonNull Resources resources, int propertyResId) {
212         String propertyName = resources.getString(propertyResId);
213         boolean isReadOnly = propertyName.startsWith(READ_ONLY_SYSTEM_PROPERTY_PREFIX);
214         if (!isReadOnly) {
215             return readSystemProperty(propertyName);
216         }
217         synchronized (READ_ONLY_SYSTEM_PROPERTY_MAP) {
218             // readOnlySystemPropertyMap may contain null values.
219             if (READ_ONLY_SYSTEM_PROPERTY_MAP.indexOfKey(propertyResId) >= 0) {
220                 return READ_ONLY_SYSTEM_PROPERTY_MAP.get(propertyResId);
221             }
222             String value = readSystemProperty(propertyName);
223             READ_ONLY_SYSTEM_PROPERTY_MAP.put(propertyResId, value);
224             return value;
225         }
226     }
227 
228     @Nullable
229     @SuppressLint("PrivateApi")
readSystemProperty(String propertyName)230     private static String readSystemProperty(String propertyName) {
231         Class<?> systemPropertiesClass;
232         try {
233             systemPropertiesClass = Class.forName("android.os.SystemProperties");
234         } catch (ClassNotFoundException e) {
235             Log.w(TAG, "Cannot find android.os.SystemProperties: ", e);
236             return null;
237         }
238 
239         Method getMethod;
240         try {
241             getMethod = systemPropertiesClass.getMethod("get", String.class);
242         } catch (NoSuchMethodException e) {
243             Log.w(TAG, "Cannot find SystemProperties.get(): ", e);
244             return null;
245         }
246 
247         try {
248             Object[] params = new Object[]{propertyName};
249             String value = (String) getMethod.invoke(systemPropertiesClass, params);
250             return TextUtils.isEmpty(value) ? null : value;
251         } catch (Exception e) {
252             Log.w(TAG, "Failed to invoke SystemProperties.get(): ", e);
253             return null;
254         }
255     }
256 
257     /**
258      * Converts a drawable to bitmap. This value should not be null.
259      */
drawableToBitmap(@onNull Drawable drawable)260     public static Bitmap drawableToBitmap(@NonNull Drawable drawable) {
261         Bitmap bitmap;
262 
263         if (drawable instanceof BitmapDrawable) {
264             BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
265             if (bitmapDrawable.getBitmap() != null) {
266                 return bitmapDrawable.getBitmap();
267             }
268         }
269 
270         if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
271             bitmap = Bitmap.createBitmap(1, 1,
272                     Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
273         } else {
274             bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
275                     drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
276         }
277 
278         Canvas canvas = new Canvas(bitmap);
279         drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
280         drawable.draw(canvas);
281         return bitmap;
282     }
283 
284     /**
285      * Exact copy from Androidx.TypedArrayUtils class
286      * @return The resource ID value in the {@code context} specified by {@code attr}. If it does
287      * not exist, {@code fallbackAttr}.
288      */
getAttr(@onNull Context context, int attr, int fallbackAttr)289     public static int getAttr(@NonNull Context context, int attr, int fallbackAttr) {
290         TypedValue value = new TypedValue();
291         context.getTheme().resolveAttribute(attr, value, true);
292         if (value.resourceId != 0) {
293             return attr;
294         }
295         return fallbackAttr;
296     }
297 
298     /**
299      * Converts a {@link CharSequence} to a {@link String}.
300      *
301      * This is the same as calling {@link CharSequence#toString()}, except it will handle
302      * null CharSequences, returning a null string.
303      */
charSequenceToString(@ullable CharSequence charSequence)304     public static String charSequenceToString(@Nullable CharSequence charSequence) {
305         return charSequence == null ? null : charSequence.toString();
306     }
307 
308     /**
309      * Given a list of T and a function to convert from T to U, return a list of U.
310      *
311      * This will create a new list.
312      */
convertList(List<T> list, Function<T, U> f)313     public static <T, U> List<U> convertList(List<T> list, Function<T, U> f) {
314         if (list == null) {
315             return null;
316         }
317 
318         List<U> result = new ArrayList<>();
319         for (T item : list) {
320             result.add(f.apply(item));
321         }
322         return result;
323     }
324 
325     /**
326      * Traverses the view hierarchy, and whenever it sees a {@link DrawableStateView}, adds
327      * state_ux_restricted to it.
328      *
329      * Note that this will remove any other drawable states added by other calls to
330      * {@link DrawableStateView#setExtraDrawableState(int[], int[])}
331      */
makeAllViewsUxRestricted(@ullable View view, boolean restricted)332     public static void makeAllViewsUxRestricted(@Nullable View view, boolean restricted) {
333         if (view == null) {
334             return;
335         }
336         initializeRestrictedState(view);
337         applyStatesToAllViews(view, restricted ? sRestrictedState : null, null);
338     }
339 
340     /**
341      * Traverses the view hierarchy, and whenever it sees a {@link DrawableStateView}, adds
342      * state_enabled to it.
343      *
344      * Note that this will remove any other drawable states added by other calls to
345      * {@link DrawableStateView#setExtraDrawableState(int[], int[])}
346      */
makeAllViewsEnabled(@ullable View view, boolean enabled)347     public static void makeAllViewsEnabled(@Nullable View view, boolean enabled) {
348         if (view == null) {
349             return;
350         }
351         initializeRestrictedState(view);
352         int[] statesToAdd = enabled ? new int[] {android.R.attr.state_enabled} : null;
353         int[] statesToRemove = enabled ? null : new int[] {android.R.attr.state_enabled};
354         applyStatesToAllViews(view, statesToAdd, statesToRemove);
355     }
356 
357     /**
358      * Traverses the view hierarchy, and whenever it sees a {@link DrawableStateView}, adds
359      * the relevant state_enabled and state_ux_restricted to the view.
360      *
361      * Note that this will remove any other drawable states added by other calls to
362      * {@link DrawableStateView#setExtraDrawableState(int[], int[])}
363      */
makeAllViewsEnabledAndUxRestricted(@ullable View view, boolean enabled, boolean restricted)364     public static void makeAllViewsEnabledAndUxRestricted(@Nullable View view, boolean enabled,
365             boolean restricted) {
366         if (view == null) {
367             return;
368         }
369         initializeRestrictedState(view);
370         int[] statesToAdd = null;
371         if (enabled) {
372             if (restricted) {
373                 statesToAdd = new int[sRestrictedState.length + 1];
374                 statesToAdd[0] = android.R.attr.state_enabled;
375                 System.arraycopy(sRestrictedState, 0, statesToAdd, 1, sRestrictedState.length);
376             } else {
377                 statesToAdd = new int[] {android.R.attr.state_enabled};
378             }
379         } else if (restricted) {
380             statesToAdd = sRestrictedState;
381         }
382         int[] statesToRemove = enabled ? null : new int[] {android.R.attr.state_enabled};
383         applyStatesToAllViews(view, statesToAdd, statesToRemove);
384     }
385 
initializeRestrictedState(@onNull View view)386     private static void initializeRestrictedState(@NonNull View view) {
387         if (sRestrictedState != null) {
388             return;
389         }
390         int androidStateUxRestricted = view.getResources()
391                 .getIdentifier("state_ux_restricted", "attr", "android");
392 
393         if (androidStateUxRestricted == 0) {
394             sRestrictedState = new int[] { R.attr.state_ux_restricted };
395         } else {
396             sRestrictedState = new int[] {
397                     R.attr.state_ux_restricted,
398                     androidStateUxRestricted
399             };
400         }
401     }
402 
applyStatesToAllViews(@onNull View view, int[] statesToAdd, int[] statesToRemove)403     private static void applyStatesToAllViews(@NonNull View view, int[] statesToAdd,
404             int[] statesToRemove) {
405         if (view instanceof DrawableStateView) {
406             ((DrawableStateView) view).setExtraDrawableState(statesToAdd, statesToRemove);
407         }
408         if (view instanceof ViewGroup) {
409             ViewGroup vg = (ViewGroup) view;
410             for (int i = 0; i < vg.getChildCount(); i++) {
411                 applyStatesToAllViews(vg.getChildAt(i), statesToAdd, statesToRemove);
412             }
413         }
414     }
415 }
416