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
diff --git a/core/tests/resourceflaggingtests/Android.bp b/core/tests/resourceflaggingtests/Android.bp
new file mode 100644
index 0000000..e8bb710
--- /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 0000000..938463b
--- /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 0000000..d6cdeb7
--- /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 0000000..f4defd9
--- /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 0000000..a0cbe3c
--- /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 7ba3277..a274f04 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -69,6 +69,8 @@
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 6af39b7..2df9418 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 @@
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 @@
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 @@
});
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 012a056..45d41c1 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 @@
// 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 a3b0b45..1cdb715 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -231,6 +231,47 @@
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 @@
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 @@
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 61e399c..9530c17 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -104,6 +104,8 @@
// 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 @@
std::optional<AllowNew> allow_new;
std::optional<StagedId> staged_id;
bool allow_mangled = false;
+ FlagStatus flag_status;
};
struct NewResourceBuilder {
@@ -282,6 +285,7 @@
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 @@
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 1d7fd1d..2ecc82a 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -246,6 +246,7 @@
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 9b8c3b3..2a978a5 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -171,6 +171,7 @@
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 e1a3013..aaab315 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 @@
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 0903205..c1e15bc 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -426,6 +426,7 @@
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 8abc26d..1527d68 100644
--- a/tools/aapt2/xml/XmlPullParser.cpp
+++ b/tools/aapt2/xml/XmlPullParser.cpp
@@ -309,7 +309,14 @@
}
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 64274d0..d65ba6f 100644
--- a/tools/aapt2/xml/XmlPullParser.h
+++ b/tools/aapt2/xml/XmlPullParser.h
@@ -194,6 +194,13 @@
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.