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