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