InlineSuggestionThemeUtils.java

/*
 * Copyright 2020 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 androidx.autofill;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class to help validate themes used to render the inline suggestion UI.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(api = Build.VERSION_CODES.Q) //TODO(b/147116534): Update to R.
public final class InlineSuggestionThemeUtils {

    private static final String TAG = "InlineThemeUtils";

    // The pattern to match the value can be obtained by calling {@code Resources#getResourceName
    // (int)}. This name is a single string of the form "package:type/entry".
    private static final Pattern RESOURCE_NAME_PATTERN = Pattern.compile("([^:]+):([^/]+)/(\S+)");

    /**
     * Returns a context wrapping the theme in the provided {@code themeName}, or fallback to the
     * default theme if the {@code themeName} doesn't pass validation.
     */
    @NonNull
    public static Context getContextThemeWrapper(@NonNull Context context,
            @Nullable String themeName) {
        Context contextThemeWrapper = maybeGetContextThemeWrapperWithStyle(context, themeName);
        if (contextThemeWrapper == null) {
            contextThemeWrapper = getDefaultContextThemeWrapper(context);
        }
        return contextThemeWrapper;
    }

    /**
     * Returns a context wrapping the theme in the provided {@code themeName}, or null if the {@code
     * themeName} doesn't pass validation.
     */
    @Nullable
    private static Context maybeGetContextThemeWrapperWithStyle(@NonNull Context context,
            @Nullable String themeName) {
        if (themeName == null) {
            return null;
        }
        Matcher matcher = RESOURCE_NAME_PATTERN.matcher(themeName);
        if (!matcher.matches()) {
            Log.d(TAG, "Can not parse the theme=" + themeName);
            return null;
        }
        String packageName = matcher.group(1);
        String type = matcher.group(2);
        String entry = matcher.group(3);
        if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(type) || TextUtils.isEmpty(entry)) {
            Log.d(TAG, "Can not proceed with empty field values in the theme=" + themeName);
            return null;
        }
        Resources resources = null;
        try {
            resources = context.getPackageManager().getResourcesForApplication(
                    packageName);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
        int resId = resources.getIdentifier(entry, type, packageName);
        if (resId == Resources.ID_NULL) {
            return null;
        }
        Resources.Theme theme = resources.newTheme();
        theme.applyStyle(resId, true);
        if (!isAutofillInlineSuggestionTheme(theme, resId)) {
            Log.d(TAG, "Provided theme is not a child of Theme.InlineSuggestion, ignoring it.");
            return null;
        }
        // TODO(b/146454892): add font checking to disallow passing in custom font.
        return new ContextThemeWrapper(context, theme);
    }

    private static Context getDefaultContextThemeWrapper(@NonNull Context context) {
        Resources.Theme theme = context.getResources().newTheme();
        theme.applyStyle(R.style.Theme_AutofillInlineSuggestion, true);
        return new ContextThemeWrapper(context, theme);
    }

    /**
     * Returns true if the provided {@code theme} is a child theme of the
     * {@code @style/Theme.AutofillInlineSuggestion}.
     */
    private static boolean isAutofillInlineSuggestionTheme(@NonNull Resources.Theme theme,
            int styleAttr) {
        TypedArray ta = null;
        try {
            ta = theme.obtainStyledAttributes(null,
                    new int[]{R.attr.isAutofillInlineSuggestionTheme}, styleAttr, 0);
            if (ta.getIndexCount() == 0) {
                return false;
            }
            return ta.getBoolean(ta.getIndex(0), false);
        } finally {
            if (ta != null) {
                ta.recycle();
            }
        }
    }

    private InlineSuggestionThemeUtils() {
    }
}