Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a35b1f13 authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "Initial system feature codegen prototype" into main

parents 31a796e3 230d6502
Loading
Loading
Loading
Loading
+63 −0
Original line number Diff line number Diff line
package {
    // See: http://go/android-license-faq
    // A large-scale-change added 'default_applicable_licenses' to import
    // all of the 'license_kinds' from "frameworks_base_license"
    // to get the below license kinds:
    //   SPDX-license-identifier-Apache-2.0
    default_applicable_licenses: ["frameworks_base_license"],
}

java_library_host {
    name: "systemfeatures-gen-lib",
    srcs: [
        "src/**/*.java",
        "src/**/*.kt",
    ],
    static_libs: [
        "guava",
        "javapoet",
    ],
}

java_binary_host {
    name: "systemfeatures-gen-tool",
    main_class: "com.android.systemfeatures.SystemFeaturesGenerator",
    static_libs: ["systemfeatures-gen-lib"],
}

// TODO(b/203143243): Add golden diff test for generated sources.
// Functional runtime behavior is covered in systemfeatures-gen-tests.
genrule {
    name: "systemfeatures-gen-tests-srcs",
    cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " +
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true > $(location RoNoFeatures.java) && " +
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RwFeatures.java) && " +
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RoFeatures.java)",
    out: [
        "RwNoFeatures.java",
        "RoNoFeatures.java",
        "RwFeatures.java",
        "RoFeatures.java",
    ],
    tools: ["systemfeatures-gen-tool"],
}

java_test_host {
    name: "systemfeatures-gen-tests",
    test_suites: ["general-tests"],
    srcs: [
        "tests/**/*.java",
        ":systemfeatures-gen-tests-srcs",
    ],
    test_options: {
        unit_test: true,
    },
    static_libs: [
        "aconfig-annotations-lib",
        "framework-annotations-lib",
        "junit",
        "objenesis",
        "mockito",
        "truth",
    ],
}
+218 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemfeatures

import com.google.common.base.CaseFormat
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.TypeSpec
import javax.lang.model.element.Modifier

/*
 * Simple Java code generator that takes as input a list of defined features and generates an
 * accessory class based on the provided versions.
 *
 * <p>Example:
 *
 * <pre>
 *   <cmd> com.foo.RoSystemFeatures --readonly=true \
 *           --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348
 * </pre>
 *
 * This generates a class that has the following signature:
 *
 * <pre>
 * package com.foo;
 * public final class RoSystemFeatures {
 *     @AssumeTrueForR8
 *     public static boolean hasFeatureWatch(Context context);
 *     @AssumeFalseForR8
 *     public static boolean hasFeatureAutomotive(Context context);
 *     @AssumeTrueForR8
 *     public static boolean hasFeatureVulkan(Context context);
 *     public static Boolean maybeHasFeature(String feature, int version);
 * }
 * </pre>
 */
object SystemFeaturesGenerator {
    private const val FEATURE_ARG = "--feature="
    private const val READONLY_ARG = "--readonly="
    private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager")
    private val CONTEXT_CLASS = ClassName.get("android.content", "Context")
    private val ASSUME_TRUE_CLASS =
        ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8")
    private val ASSUME_FALSE_CLASS =
        ClassName.get("com.android.aconfig.annotations", "AssumeFalseForR8")

    private fun usage() {
        println("Usage: SystemFeaturesGenerator <outputClassName> [options]")
        println(" Options:")
        println("  --readonly=true|false    Whether to encode features as build-time constants")
        println("  --feature=\$NAME:\$VER   A feature+version pair (blank version == disabled)")
    }

    /** Main entrypoint for build-time system feature codegen. */
    @JvmStatic
    fun main(args: Array<String>) {
        if (args.size < 1) {
            usage()
            return
        }

        var readonly = false
        var outputClassName: ClassName? = null
        val features = mutableListOf<FeatureInfo>()
        for (arg in args) {
            when {
                arg.startsWith(READONLY_ARG) ->
                    readonly = arg.substring(READONLY_ARG.length).toBoolean()
                arg.startsWith(FEATURE_ARG) -> {
                    features.add(parseFeatureArg(arg))
                }
                else -> outputClassName = ClassName.bestGuess(arg)
            }
        }

        outputClassName
            ?: run {
                println("Output class name must be provided.")
                usage()
                return
            }

        val classBuilder =
            TypeSpec.classBuilder(outputClassName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addJavadoc("@hide")

        addFeatureMethodsToClass(classBuilder, readonly, features)
        addMaybeFeatureMethodToClass(classBuilder, readonly, features)

        // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency.
        JavaFile.builder(outputClassName.packageName(), classBuilder.build())
            .build()
            .writeTo(System.out)
    }

    /*
     * Parses a feature argument of the form "--feature=$NAME:$VER", where "$VER" is optional.
     *   * "--feature=WATCH:0" -> Feature enabled w/ version 0 (default version when enabled)
     *   * "--feature=WATCH:7" -> Feature enabled w/ version 7
     *   * "--feature=WATCH:"  -> Feature disabled
     */
    private fun parseFeatureArg(arg: String): FeatureInfo {
        val featureArgs = arg.substring(FEATURE_ARG.length).split(":")
        val name = featureArgs[0].let { if (!it.startsWith("FEATURE_")) "FEATURE_$it" else it }
        val version = featureArgs.getOrNull(1)?.toIntOrNull()
        return FeatureInfo(name, version)
    }

    /*
     * Adds per-feature query methods to the class with the form:
     * {@code public static boolean hasFeatureX(Context context)},
     * returning the fallback value from PackageManager if not readonly.
     */
    private fun addFeatureMethodsToClass(
        builder: TypeSpec.Builder,
        readonly: Boolean,
        features: List<FeatureInfo>
    ) {
        for (feature in features) {
            // Turn "FEATURE_FOO" into "hasFeatureFoo".
            val methodName =
                "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, feature.name)
            val methodBuilder =
                MethodSpec.methodBuilder(methodName)
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(Boolean::class.java)
                    .addParameter(CONTEXT_CLASS, "context")

            if (readonly) {
                val featureEnabled = compareValues(feature.version, 0) >= 0
                methodBuilder.addAnnotation(
                    if (featureEnabled) ASSUME_TRUE_CLASS else ASSUME_FALSE_CLASS
                )
                methodBuilder.addStatement("return $featureEnabled")
            } else {
                methodBuilder.addStatement(
                    "return hasFeatureFallback(context, \$T.\$N)",
                    PACKAGEMANAGER_CLASS,
                    feature.name
                )
            }
            builder.addMethod(methodBuilder.build())
        }

        if (!readonly) {
            builder.addMethod(
                MethodSpec.methodBuilder("hasFeatureFallback")
                    .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
                    .returns(Boolean::class.java)
                    .addParameter(CONTEXT_CLASS, "context")
                    .addParameter(String::class.java, "featureName")
                    .addStatement(
                        "return context.getPackageManager().hasSystemFeature(featureName, 0)"
                    )
                    .build()
            )
        }
    }

    /*
     * Adds a generic query method to the class with the form: {@code public static boolean
     * maybeHasFeature(String featureName, int version)}, returning null if the feature version is
     * undefined or not readonly.
     *
     * This method is useful for internal usage within the framework, e.g., from the implementation
     * of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only
     * want a valid result if it's defined as readonly, and we want a custom fallback otherwise
     * (e.g., to the existing runtime binder query).
     */
    private fun addMaybeFeatureMethodToClass(
        builder: TypeSpec.Builder,
        readonly: Boolean,
        features: List<FeatureInfo>
    ) {
        val methodBuilder =
            MethodSpec.methodBuilder("maybeHasFeature")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addAnnotation(ClassName.get("android.annotation", "Nullable"))
                .returns(Boolean::class.javaObjectType) // Use object type for nullability
                .addParameter(String::class.java, "featureName")
                .addParameter(Int::class.java, "version")

        if (readonly) {
            methodBuilder.beginControlFlow("switch (featureName)")
            for (feature in features) {
                methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name)
                if (feature.version != null) {
                    methodBuilder.addStatement("return \$L >= version", feature.version)
                } else {
                    methodBuilder.addStatement("return false")
                }
            }
            methodBuilder.addCode("default: ")
            methodBuilder.addStatement("break")
            methodBuilder.endControlFlow()
        }
        methodBuilder.addStatement("return null")
        builder.addMethod(methodBuilder.build())
    }

    private data class FeatureInfo(val name: String, val version: Int?)
}
+27 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 android.content;

import android.content.pm.PackageManager;

/** Stub for testing. */
public class Context {
    /** @hide */
    public PackageManager getPackageManager() {
        return null;
    }
}
+30 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 android.content.pm;

/** Stub for testing */
public class PackageManager {
    public static final String FEATURE_AUTO = "automotive";
    public static final String FEATURE_VULKAN = "vulkan";
    public static final String FEATURE_WATCH = "watch";
    public static final String FEATURE_WIFI = "wifi";

    /** @hide */
    public boolean hasSystemFeature(String featureName, int version) {
        return false;
    }
}
+135 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemfeatures;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.pm.PackageManager;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

@RunWith(JUnit4.class)
public class SystemFeaturesGeneratorTest {

    @Rule public final MockitoRule mockito = MockitoJUnit.rule();

    @Mock private Context mContext;
    @Mock private PackageManager mPackageManager;

    @Before
    public void setUp() {
        when(mContext.getPackageManager()).thenReturn(mPackageManager);
    }

    @Test
    public void testReadonlyDisabledNoDefinedFeatures() {
        // Always report null for conditional queries if readonly codegen is disabled.
        assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull();
        assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull();
        assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
        assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
        assertThat(RwNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
    }

    @Test
    public void testReadonlyNoDefinedFeatures() {
        // If no features are explicitly declared as readonly available, always report
        // null for conditional queries.
        assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull();
        assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull();
        assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
        assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
        assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
    }

    @Test
    public void testReadonlyDisabledWithDefinedFeatures() {
        // Always fall back to the PackageManager for defined, explicit features queries.
        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true);
        assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue();

        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false);
        assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse();

        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true);
        assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue();

        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false);
        assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse();

        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false);
        assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse();

        // For defined and undefined features, conditional queries should report null (unknown).
        assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull();
        assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull();
        assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
        assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
        assertThat(RwFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
    }

    @Test
    public void testReadonlyWithDefinedFeatures() {
        // Always use the build-time feature version for defined, explicit feature queries, never
        // falling back to the runtime query.
        assertThat(RoFeatures.hasFeatureWatch(mContext)).isTrue();
        assertThat(RoFeatures.hasFeatureWifi(mContext)).isTrue();
        assertThat(RoFeatures.hasFeatureVulkan(mContext)).isFalse();
        assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse();
        verify(mPackageManager, never()).hasSystemFeature(anyString(), anyInt());

        // For defined feature types, conditional queries should reflect the build-time versions.
        // VERSION=1
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, -1)).isTrue();
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isTrue();
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 100)).isFalse();

        // VERSION=0
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, -1)).isTrue();
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isTrue();
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 100)).isFalse();

        // VERSION=-1
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, -1)).isTrue();
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isFalse();
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse();

        // DISABLED
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isFalse();
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isFalse();
        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 100)).isFalse();

        // For undefined types, conditional queries should report null (unknown).
        assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", -1)).isNull();
        assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
        assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 100)).isNull();
    }
}