diff options
author | 2024-06-04 14:22:03 -0700 | |
---|---|---|
committer | 2024-07-12 13:29:55 -0700 | |
commit | 211bec2871da597f9f3fd81df7faffea1754437e (patch) | |
tree | cb65f77cf0dd3810f068efd20937b0745438ad12 | |
parent | a1c0fd0b31b799796295c648ae87c83d2e5ce470 (diff) |
First pass at flagged resources
This gets the main parts of resource flagging in place and the basic use
case of flagging with an xml attribute working.
Test: Automated
Bug: 329436914
Flag: EXEMPT Aconfig not supported on host tools
Change-Id: Id2b5ba450d05da00a922e98ca204b6e5aa6c6c24
-rw-r--r-- | core/tests/resourceflaggingtests/Android.bp | 75 | ||||
-rw-r--r-- | core/tests/resourceflaggingtests/AndroidManifest.xml | 31 | ||||
-rw-r--r-- | core/tests/resourceflaggingtests/TestAppAndroidManifest.xml | 4 | ||||
-rw-r--r-- | core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml | 8 | ||||
-rw-r--r-- | core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java | 81 | ||||
-rw-r--r-- | tools/aapt2/Resource.h | 2 | ||||
-rw-r--r-- | tools/aapt2/ResourceParser.cpp | 28 | ||||
-rw-r--r-- | tools/aapt2/ResourceParser.h | 3 | ||||
-rw-r--r-- | tools/aapt2/ResourceTable.cpp | 55 | ||||
-rw-r--r-- | tools/aapt2/ResourceTable.h | 7 | ||||
-rw-r--r-- | tools/aapt2/Resources.proto | 1 | ||||
-rw-r--r-- | tools/aapt2/cmd/Compile.cpp | 1 | ||||
-rw-r--r-- | tools/aapt2/format/proto/ProtoDeserialize.cpp | 3 | ||||
-rw-r--r-- | tools/aapt2/format/proto/ProtoSerialize.cpp | 1 | ||||
-rw-r--r-- | tools/aapt2/xml/XmlPullParser.cpp | 9 | ||||
-rw-r--r-- | tools/aapt2/xml/XmlPullParser.h | 7 |
16 files changed, 311 insertions, 5 deletions
diff --git a/core/tests/resourceflaggingtests/Android.bp b/core/tests/resourceflaggingtests/Android.bp new file mode 100644 index 000000000000..e8bb71010006 --- /dev/null +++ b/core/tests/resourceflaggingtests/Android.bp @@ -0,0 +1,75 @@ +// 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 { + // 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"], + default_team: "trendy_team_android_resources", +} + +genrule { + name: "resource-flagging-test-app-resources-compile", + tools: ["aapt2"], + srcs: [ + "flagged_resources_res/values/bools.xml", + ], + out: ["values_bools.arsc.flat"], + cmd: "$(location aapt2) compile $(in) -o $(genDir) " + + "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true", +} + +genrule { + name: "resource-flagging-test-app-apk", + tools: ["aapt2"], + // The first input file in the list must be the manifest + srcs: [ + "TestAppAndroidManifest.xml", + ":resource-flagging-test-app-resources-compile", + ], + out: ["resapp.apk"], + cmd: "$(location aapt2) link -o $(out) --manifest $(in)", +} + +java_genrule { + name: "resource-flagging-apk-as-resource", + srcs: [ + ":resource-flagging-test-app-apk", + ], + out: ["apks_as_resources.res.zip"], + tools: ["soong_zip"], + + cmd: "mkdir -p $(genDir)/res/raw && " + + "cp $(in) $(genDir)/res/raw/$$(basename $(in)) && " + + "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res", +} + +android_test { + name: "ResourceFlaggingTests", + srcs: [ + "src/**/*.java", + ], + platform_apis: true, + certificate: "platform", + static_libs: [ + "androidx.test.rules", + "testng", + "compatibility-device-util-axt", + ], + resource_zips: [":resource-flagging-apk-as-resource"], + test_suites: ["device-tests"], +} diff --git a/core/tests/resourceflaggingtests/AndroidManifest.xml b/core/tests/resourceflaggingtests/AndroidManifest.xml new file mode 100644 index 000000000000..938463b14e64 --- /dev/null +++ b/core/tests/resourceflaggingtests/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.resourceflaggingtests"> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.resourceflaggingtests" + android:label="Resource Flagging Tests" /> + +</manifest>
\ No newline at end of file diff --git a/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml b/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml new file mode 100644 index 000000000000..d6cdeb7b5231 --- /dev/null +++ b/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml @@ -0,0 +1,4 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.intenal.flaggedresources"> + <application/> +</manifest>
\ No newline at end of file diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml b/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml new file mode 100644 index 000000000000..f4defd94831c --- /dev/null +++ b/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <bool name="res1">true</bool> + <bool name="res1" android:featureFlag="test.package.falseFlag">false</bool> + + <bool name="res2">false</bool> + <bool name="res2" android:featureFlag="test.package.trueFlag">true</bool> +</resources>
\ No newline at end of file diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java new file mode 100644 index 000000000000..a0cbe3cf05c3 --- /dev/null +++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java @@ -0,0 +1,81 @@ +/* + * 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.resourceflaggingtests; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.FileUtils; +import android.util.DisplayMetrics; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.InputStream; + +@RunWith(JUnit4.class) +@SmallTest +public class ResourceFlaggingTest { + private Context mContext; + private Resources mResources; + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getTargetContext(); + AssetManager assets = new AssetManager(); + assertThat(assets.addAssetPath(extractApkAndGetPath(R.raw.resapp))).isNotEqualTo(0); + + final DisplayMetrics dm = new DisplayMetrics(); + dm.setToDefaults(); + mResources = new Resources(assets, dm, new Configuration()); + } + + @Test + public void testFlagDisabled() { + assertThat(getBoolean("res1")).isTrue(); + } + + @Test + public void testFlagEnabled() { + assertThat(getBoolean("res2")).isTrue(); + } + + private boolean getBoolean(String name) { + int resId = mResources.getIdentifier(name, "bool", "com.android.intenal.flaggedresources"); + assertThat(resId).isNotEqualTo(0); + return mResources.getBoolean(resId); + } + + private String extractApkAndGetPath(int id) throws Exception { + final Resources resources = mContext.getResources(); + try (InputStream is = resources.openRawResource(id)) { + File path = new File(mContext.getFilesDir(), resources.getResourceEntryName(id)); + path.deleteOnExit(); + FileUtils.copyToFileOrThrow(is, path); + return path.getAbsolutePath(); + } + } +} diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index 7ba3277d2093..a274f047586c 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -69,6 +69,8 @@ enum class ResourceType { kXml, }; +enum class FlagStatus { NoFlag = 0, Disabled = 1, Enabled = 2 }; + android::StringPiece to_string(ResourceType type); /** diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index 6af39b739e9b..2df941834063 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - #include "ResourceParser.h" #include <functional> @@ -108,6 +107,7 @@ struct ParsedResource { Visibility::Level visibility_level = Visibility::Level::kUndefined; bool staged_api = false; bool allow_new = false; + FlagStatus flag_status; std::optional<OverlayableItem> overlayable_item; std::optional<StagedId> staged_alias; @@ -161,6 +161,8 @@ static bool AddResourcesToTable(ResourceTable* table, android::IDiagnostics* dia res_builder.SetStagedId(res->staged_alias.value()); } + res_builder.SetFlagStatus(res->flag_status); + bool error = false; if (!res->name.entry.empty()) { if (!table->AddResource(res_builder.Build(), diag)) { @@ -544,6 +546,30 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, }); std::string resource_type = parser->element_name(); + std::optional<StringPiece> flag = + xml::FindAttribute(parser, "http://schemas.android.com/apk/res/android", "featureFlag"); + out_resource->flag_status = FlagStatus::NoFlag; + if (flag) { + auto flag_it = options_.feature_flag_values.find(flag.value()); + if (flag_it == options_.feature_flag_values.end()) { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) + << "Resource flag value undefined"); + return false; + } + const auto& flag_properties = flag_it->second; + if (!flag_properties.read_only) { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) + << "Only read only flags may be used with resources"); + return false; + } + if (!flag_properties.enabled.has_value()) { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) + << "Only flags with a value may be used with resources"); + return false; + } + out_resource->flag_status = + flag_properties.enabled.value() ? FlagStatus::Enabled : FlagStatus::Disabled; + } // The value format accepted for this resource. uint32_t resource_format = 0u; diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h index 012a056dccf3..45d41c193cb4 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -27,6 +27,7 @@ #include "androidfw/IDiagnostics.h" #include "androidfw/StringPiece.h" #include "androidfw/StringPool.h" +#include "cmd/Util.h" #include "xml/XmlPullParser.h" namespace aapt { @@ -54,6 +55,8 @@ struct ResourceParserOptions { // If visibility was forced, we need to use it when creating a new resource and also error if we // try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags. std::optional<Visibility::Level> visibility; + + FeatureFlagValues feature_flag_values; }; struct FlattenedXmlSubTree { diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index a3b0b45df5c3..1cdb71551d5d 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -231,6 +231,47 @@ bool ResourceEntry::HasDefaultValue() const { return false; } +ResourceTable::CollisionResult ResourceTable::ResolveFlagCollision(FlagStatus existing, + FlagStatus incoming) { + switch (existing) { + case FlagStatus::NoFlag: + switch (incoming) { + case FlagStatus::NoFlag: + return CollisionResult::kConflict; + case FlagStatus::Disabled: + return CollisionResult::kKeepOriginal; + case FlagStatus::Enabled: + return CollisionResult::kTakeNew; + default: + return CollisionResult::kConflict; + } + case FlagStatus::Disabled: + switch (incoming) { + case FlagStatus::NoFlag: + return CollisionResult::kTakeNew; + case FlagStatus::Disabled: + return CollisionResult::kKeepOriginal; + case FlagStatus::Enabled: + return CollisionResult::kTakeNew; + default: + return CollisionResult::kConflict; + } + case FlagStatus::Enabled: + switch (incoming) { + case FlagStatus::NoFlag: + return CollisionResult::kKeepOriginal; + case FlagStatus::Disabled: + return CollisionResult::kKeepOriginal; + case FlagStatus::Enabled: + return CollisionResult::kConflict; + default: + return CollisionResult::kConflict; + } + default: + return CollisionResult::kConflict; + } +} + // The default handler for collisions. // // Typically, a weak value will be overridden by a strong value. An existing weak @@ -564,16 +605,21 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) if (!config_value->value) { // Resource does not exist, add it now. config_value->value = std::move(res.value); + config_value->flag_status = res.flag_status; } else { // When validation is enabled, ensure that a resource cannot have multiple values defined for - // the same configuration. - auto result = validate ? ResolveValueCollision(config_value->value.get(), res.value.get()) + // the same configuration unless protected by flags. + auto result = validate ? ResolveFlagCollision(config_value->flag_status, res.flag_status) : CollisionResult::kKeepBoth; + if (result == CollisionResult::kConflict) { + result = ResolveValueCollision(config_value->value.get(), res.value.get()); + } switch (result) { case CollisionResult::kKeepBoth: // Insert the value ignoring for duplicate configurations entry->values.push_back(util::make_unique<ResourceConfigValue>(res.config, res.product)); entry->values.back()->value = std::move(res.value); + entry->values.back()->flag_status = res.flag_status; break; case CollisionResult::kTakeNew: @@ -735,6 +781,11 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) { return *this; } +NewResourceBuilder& NewResourceBuilder::SetFlagStatus(FlagStatus flag_status) { + res_.flag_status = flag_status; + return *this; +} + NewResource NewResourceBuilder::Build() { return std::move(res_); } diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index 61e399c7ab68..9530c1750c79 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -104,6 +104,8 @@ class ResourceConfigValue { // The actual Value. std::unique_ptr<Value> value; + FlagStatus flag_status; + ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product) : config(config), product(product) { } @@ -269,6 +271,7 @@ struct NewResource { std::optional<AllowNew> allow_new; std::optional<StagedId> staged_id; bool allow_mangled = false; + FlagStatus flag_status; }; struct NewResourceBuilder { @@ -282,6 +285,7 @@ struct NewResourceBuilder { NewResourceBuilder& SetAllowNew(AllowNew allow_new); NewResourceBuilder& SetStagedId(StagedId id); NewResourceBuilder& SetAllowMangled(bool allow_mangled); + NewResourceBuilder& SetFlagStatus(FlagStatus flag_status); NewResource Build(); private: @@ -330,7 +334,8 @@ class ResourceTable { std::unique_ptr<ResourceTable> Clone() const; - // When a collision of resources occurs, this method decides which value to keep. + // When a collision of resources occurs, these methods decide which value to keep. + static CollisionResult ResolveFlagCollision(FlagStatus existing, FlagStatus incoming); static CollisionResult ResolveValueCollision(Value* existing, Value* incoming); // The string pool used by this resource table. Values that reference strings must use diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto index 1d7fd1d17dcd..2ecc82ae4792 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -246,6 +246,7 @@ message Entry { message ConfigValue { Configuration config = 1; Value value = 2; + uint32 flag_status = 3; } // The generic meta-data for every value in a resource table. diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 9b8c3b3d549c..2a978a5153ca 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -171,6 +171,7 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, parser_options.error_on_positional_arguments = !options.legacy_mode; parser_options.preserve_visibility_of_styleables = options.preserve_visibility_of_styleables; parser_options.translatable = translatable_file; + parser_options.feature_flag_values = options.feature_flag_values; // If visibility was forced, we need to use it when creating a new resource and also error if // we try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags. diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp index e1a3013c07bb..aaab3158f61e 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -16,6 +16,7 @@ #include "format/proto/ProtoDeserialize.h" +#include "Resource.h" #include "ResourceTable.h" #include "ResourceUtils.h" #include "ResourceValues.h" @@ -533,6 +534,8 @@ static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStr return false; } + config_value->flag_status = (FlagStatus)pb_config_value.flag_status(); + config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config, &out_table->string_pool, files, out_error); if (config_value->value == nullptr) { diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp index 0903205b5eb2..c1e15bcf9f70 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -426,6 +426,7 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table pb_config_value->mutable_config()->set_product(config_value->product); SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(), source_pool.get()); + pb_config_value->set_flag_status((uint32_t)config_value->flag_status); } } } diff --git a/tools/aapt2/xml/XmlPullParser.cpp b/tools/aapt2/xml/XmlPullParser.cpp index 8abc26d67c1f..1527d68a6c3b 100644 --- a/tools/aapt2/xml/XmlPullParser.cpp +++ b/tools/aapt2/xml/XmlPullParser.cpp @@ -309,7 +309,14 @@ void XMLCALL XmlPullParser::EndCdataSectionHandler(void* user_data) { } std::optional<StringPiece> FindAttribute(const XmlPullParser* parser, StringPiece name) { - auto iter = parser->FindAttribute("", name); + return FindAttribute(parser, "", name); +} + +std::optional<android::StringPiece> FindAttribute(const XmlPullParser* parser, + android::StringPiece namespace_uri, + android::StringPiece name) { + auto iter = parser->FindAttribute(namespace_uri, name); + if (iter != parser->end_attributes()) { return StringPiece(util::TrimWhitespace(iter->value)); } diff --git a/tools/aapt2/xml/XmlPullParser.h b/tools/aapt2/xml/XmlPullParser.h index 64274d032c61..d65ba6fb56e3 100644 --- a/tools/aapt2/xml/XmlPullParser.h +++ b/tools/aapt2/xml/XmlPullParser.h @@ -194,6 +194,13 @@ std::optional<android::StringPiece> FindAttribute(const XmlPullParser* parser, android::StringPiece name); /** + * Finds the attribute in the current element within the given namespace. + */ +std::optional<android::StringPiece> FindAttribute(const XmlPullParser* parser, + android::StringPiece namespace_uri, + android::StringPiece name); + +/** * Finds the attribute in the current element within the global namespace. The * attribute's value * must not be the empty string. |