blob: 54d22fa15912c052a85a7d44f29ca6a65bd1ed7d [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.ui.utils;
import static com.android.car.ui.core.CarUi.MIN_TARGET_API;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.DimenRes;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.annotation.UiThread;
import com.android.car.ui.R;
import com.android.car.ui.uxr.DrawableStateView;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
/**
* Collection of utility methods
*/
@SuppressWarnings("AndroidJdkLibsChecker")
@TargetApi(MIN_TARGET_API)
public final class CarUiUtils {
private static final String TAG = "CarUiUtils";
private static final String READ_ONLY_SYSTEM_PROPERTY_PREFIX = "ro.";
/** A map to cache read-only system properties. */
private static final SparseArray<String> READ_ONLY_SYSTEM_PROPERTY_MAP = new SparseArray<>();
private static int[] sRestrictedState;
/** This is a utility class */
private CarUiUtils() {
}
/**
* Reads a float value from a dimens resource. This is necessary as {@link Resources#getFloat}
* is not currently public.
*
* @param res {@link Resources} to read values from
* @param resId Id of the dimens resource to read
*/
public static float getFloat(Resources res, @DimenRes int resId) {
TypedValue outValue = new TypedValue();
res.getValue(resId, outValue, true);
return outValue.getFloat();
}
/** Returns the identifier of the resolved resource assigned to the given attribute. */
public static int getAttrResourceId(Context context, int attr) {
return getAttrResourceId(context, /*styleResId=*/ 0, attr);
}
/**
* Returns the identifier of the resolved resource assigned to the given attribute defined in
* the given style.
*/
public static int getAttrResourceId(Context context, @StyleRes int styleResId, int attr) {
TypedArray ta = context.obtainStyledAttributes(styleResId, new int[]{attr});
int resId = ta.getResourceId(0, 0);
ta.recycle();
return resId;
}
/**
* Gets the boolean value of an Attribute from an {@link Activity Activity's}
* {@link android.content.res.Resources.Theme}.
*/
public static boolean getThemeBoolean(Activity activity, int attr) {
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr});
try {
return a.getBoolean(0, false);
} finally {
a.recycle();
}
}
/**
* Gets the {@link Activity} for a certain {@link Context}.
*
* <p>It is possible the Context is not associated with an Activity, in which case
* this method will return null.
*/
@Nullable
public static Activity getActivity(@Nullable Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
return null;
}
/**
* It behaves similarly to {@link View#findViewById(int)}, except that on Q and below,
* it will first resolve the id to whatever it references.
*
* This is to support layout RROs before the new RRO features in R.
*
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
* @see View#requireViewById(int)
*/
@Nullable
@UiThread
@SuppressWarnings("TypeParameterUnusedInFormals")
public static <T extends View> T findViewByRefId(@NonNull View root, @IdRes int id) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return root.findViewById(id);
}
if (id == View.NO_ID) {
return null;
}
TypedValue value = new TypedValue();
root.getResources().getValue(id, value, true);
return root.findViewById(value.resourceId);
}
/**
* It behaves similarly to {@link View#requireViewById(int)}, except that on Q and below,
* it will first resolve the id to whatever it references.
*
* This is to support layout RROs before the new RRO features in R.
*
* @param id the ID to search for
* @return a view with given ID
* @see View#findViewById(int)
*/
@NonNull
@UiThread
@SuppressWarnings("TypeParameterUnusedInFormals")
public static <T extends View> T requireViewByRefId(@NonNull View root, @IdRes int id) {
T view = findViewByRefId(root, id);
if (view == null) {
throw new IllegalArgumentException("ID "
+ root.getResources().getResourceName(id)
+ " does not reference a View inside this View");
}
return view;
}
/**
* Returns the system property of type boolean. This method converts the boolean value in string
* returned by {@link #getSystemProperty(Resources, int)}
*/
public static boolean getBooleanSystemProperty(
@NonNull Resources resources, int propertyResId, boolean defaultValue) {
String value = getSystemProperty(resources, propertyResId);
if (!TextUtils.isEmpty(value)) {
return Boolean.parseBoolean(value);
}
return defaultValue;
}
/**
* Use reflection to interact with the hidden API <code>android.os.SystemProperties</code>.
*
* <p>This method caches read-only properties. CAVEAT: Please do not set read-only properties
* by 'adb setprop' after app started. Read-only properties CAN BE SET ONCE if it is unset.
* Thus, read-only properties MAY BE CHANGED from unset to set during application's lifetime if
* you use 'adb setprop' command to set read-only properties after app started. For the sake of
* performance, this method also caches the unset state. Otherwise, cache may not effective if
* the system property is unset (which is most-likely).
*
* @param resources resources object to fetch string
* @param propertyResId the property resource id.
* @return The value of the property if defined, else null. Does not return empty strings.
*/
@Nullable
public static String getSystemProperty(@NonNull Resources resources, int propertyResId) {
String propertyName = resources.getString(propertyResId);
boolean isReadOnly = propertyName.startsWith(READ_ONLY_SYSTEM_PROPERTY_PREFIX);
if (!isReadOnly) {
return readSystemProperty(propertyName);
}
synchronized (READ_ONLY_SYSTEM_PROPERTY_MAP) {
// readOnlySystemPropertyMap may contain null values.
if (READ_ONLY_SYSTEM_PROPERTY_MAP.indexOfKey(propertyResId) >= 0) {
return READ_ONLY_SYSTEM_PROPERTY_MAP.get(propertyResId);
}
String value = readSystemProperty(propertyName);
READ_ONLY_SYSTEM_PROPERTY_MAP.put(propertyResId, value);
return value;
}
}
@Nullable
@SuppressLint("PrivateApi")
private static String readSystemProperty(String propertyName) {
Class<?> systemPropertiesClass;
try {
systemPropertiesClass = Class.forName("android.os.SystemProperties");
} catch (ClassNotFoundException e) {
Log.w(TAG, "Cannot find android.os.SystemProperties: ", e);
return null;
}
Method getMethod;
try {
getMethod = systemPropertiesClass.getMethod("get", String.class);
} catch (NoSuchMethodException e) {
Log.w(TAG, "Cannot find SystemProperties.get(): ", e);
return null;
}
try {
Object[] params = new Object[]{propertyName};
String value = (String) getMethod.invoke(systemPropertiesClass, params);
return TextUtils.isEmpty(value) ? null : value;
} catch (Exception e) {
Log.w(TAG, "Failed to invoke SystemProperties.get(): ", e);
return null;
}
}
/**
* Converts a drawable to bitmap. This value should not be null.
*/
public static Bitmap drawableToBitmap(@NonNull Drawable drawable) {
Bitmap bitmap;
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
if (bitmapDrawable.getBitmap() != null) {
return bitmapDrawable.getBitmap();
}
}
if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
bitmap = Bitmap.createBitmap(1, 1,
Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
/**
* Exact copy from Androidx.TypedArrayUtils class
* @return The resource ID value in the {@code context} specified by {@code attr}. If it does
* not exist, {@code fallbackAttr}.
*/
public static int getAttr(@NonNull Context context, int attr, int fallbackAttr) {
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(attr, value, true);
if (value.resourceId != 0) {
return attr;
}
return fallbackAttr;
}
/**
* Converts a {@link CharSequence} to a {@link String}.
*
* This is the same as calling {@link CharSequence#toString()}, except it will handle
* null CharSequences, returning a null string.
*/
public static String charSequenceToString(@Nullable CharSequence charSequence) {
return charSequence == null ? null : charSequence.toString();
}
/**
* Given a list of T and a function to convert from T to U, return a list of U.
*
* This will create a new list.
*/
public static <T, U> List<U> convertList(List<T> list, Function<T, U> f) {
if (list == null) {
return null;
}
List<U> result = new ArrayList<>();
for (T item : list) {
result.add(f.apply(item));
}
return result;
}
/**
* Traverses the view hierarchy, and whenever it sees a {@link DrawableStateView}, adds
* state_ux_restricted to it.
*
* Note that this will remove any other drawable states added by other calls to
* {@link DrawableStateView#setExtraDrawableState(int[], int[])}
*/
public static void makeAllViewsUxRestricted(@Nullable View view, boolean restricted) {
if (view == null) {
return;
}
initializeRestrictedState(view);
applyStatesToAllViews(view, restricted ? sRestrictedState : null, null);
}
/**
* Traverses the view hierarchy, and whenever it sees a {@link DrawableStateView}, adds
* the relevant state_enabled and state_ux_restricted to the view.
*
* Note that this will remove any other drawable states added by other calls to
* {@link DrawableStateView#setExtraDrawableState(int[], int[])}
*/
public static void makeAllViewsEnabledAndUxRestricted(@Nullable View view, boolean enabled,
boolean restricted) {
if (view == null) {
return;
}
initializeRestrictedState(view);
int[] statesToAdd = null;
if (enabled) {
if (restricted) {
statesToAdd = new int[sRestrictedState.length + 1];
statesToAdd[0] = android.R.attr.state_enabled;
System.arraycopy(sRestrictedState, 0, statesToAdd, 1, sRestrictedState.length);
} else {
statesToAdd = new int[] {android.R.attr.state_enabled};
}
} else if (restricted) {
statesToAdd = sRestrictedState;
}
int[] statesToRemove = enabled ? null : new int[] {android.R.attr.state_enabled};
applyStatesToAllViews(view, statesToAdd, statesToRemove);
}
private static void initializeRestrictedState(@NonNull View view) {
if (sRestrictedState != null) {
return;
}
int androidStateUxRestricted = view.getResources()
.getIdentifier("state_ux_restricted", "attr", "android");
if (androidStateUxRestricted == 0) {
sRestrictedState = new int[] { R.attr.state_ux_restricted };
} else {
sRestrictedState = new int[] {
R.attr.state_ux_restricted,
androidStateUxRestricted
};
}
}
private static void applyStatesToAllViews(@NonNull View view, int[] statesToAdd,
int[] statesToRemove) {
if (view instanceof DrawableStateView) {
((DrawableStateView) view).setExtraDrawableState(statesToAdd, statesToRemove);
}
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
for (int i = 0; i < vg.getChildCount(); i++) {
applyStatesToAllViews(vg.getChildAt(i), statesToAdd, statesToRemove);
}
}
}
}