Replace --revert-annotation with a flags config file

Previously, Metalava did not support flags specifically and instead
relied on the build to generate the appropriate `--revert-annotation`
options to ensure that each flagged API was reverted or finalized as
appropriate to the flag state. However, Metalava now supports
configuring the flag state explicitly so this change switches the build
to make use of that.

The set of flags to be passed to Metalava are generated using exactly
the same queries as before except that instead of just outputting the
`{fully_qualified_name}` it outputs the whole protobuf definition.
The `keep-flagged-apis.sh` script has been replaced by
`aconfig-to-metalava-flags.py` which consumes the protobuf and
generates a config file suitable for Metalava to consume. Finally,
instead of using `@<file>` syntax to pass a file containing command
line options the config file is specified using `--config-file`.

One point to note, previously, when `aconfigFlagsPaths` was empty it
would just pass `--revert-annotation android.annotation.FlaggedApi` to
revert them all. To do the same with the configuration file it is
necessary to pass an empty `<api-flags/>` as no `<api-flags>` means
keep all the flagged APIs, just like no `--revert-annotation` does.
Rather than have some special logic to handle that case and create an
empty file I just let it drop through as the code below generates an
empty protobuf file which in turn creates an empty `<api-flags/>`
element in the config file.

Bug: 399846595
Test: # Before applying this change run:
      tools/metalava/scripts/gather-android-metalava-artifacts.py before
      # After applying this change run:
      tools/metalava/scripts/gather-android-metalava-artifacts.py after
      # Make sure that there are no differences.
      meld before after
Change-Id: Ia95e111f10d066996e4e51c5b7027b09f4d05d57
diff --git a/apex/apex_test.go b/apex/apex_test.go
index 9eaf814..e7a8de9 100644
--- a/apex/apex_test.go
+++ b/apex/apex_test.go
@@ -11667,7 +11667,7 @@
 
 	// Arguments passed to aconfig to retrieve the state of the flags defined in the
 	// textproto files
-	aconfigFlagArgs := m.Output("released-flagged-apis-exportable.txt").Args["flags_path"]
+	aconfigFlagArgs := m.Output("released-flags-exportable.pb").Args["flags_path"]
 
 	// "bar-lib" is a static_lib of "foo" and is passed to metalava as classpath. Thus the
 	// cache file provided by the associated aconfig_declarations module "bar" should be passed
diff --git a/java/builder.go b/java/builder.go
index ade9784..dff0032 100644
--- a/java/builder.go
+++ b/java/builder.go
@@ -310,7 +310,7 @@
 
 	gatherReleasedFlaggedApisRule = pctx.AndroidStaticRule("gatherReleasedFlaggedApisRule",
 		blueprint.RuleParams{
-			Command: `${aconfig} dump-cache --dedup --format='{fully_qualified_name}' ` +
+			Command: `${aconfig} dump-cache --dedup --format=protobuf ` +
 				`--out ${out} ` +
 				`${flags_path} ` +
 				`${filter_args} `,
@@ -320,8 +320,8 @@
 
 	generateMetalavaRevertAnnotationsRule = pctx.AndroidStaticRule("generateMetalavaRevertAnnotationsRule",
 		blueprint.RuleParams{
-			Command:     `${keep-flagged-apis} ${in} > ${out}`,
-			CommandDeps: []string{"${keep-flagged-apis}"},
+			Command:     `${aconfig-to-metalava-flags} ${in} > ${out}`,
+			CommandDeps: []string{"${aconfig-to-metalava-flags}"},
 		})
 
 	generateApiXMLRule = pctx.AndroidStaticRule("generateApiXMLRule",
@@ -339,7 +339,7 @@
 	pctx.HostBinToolVariable("aconfig", "aconfig")
 	pctx.HostBinToolVariable("ravenizer", "ravenizer")
 	pctx.HostBinToolVariable("apimapper", "apimapper")
-	pctx.HostBinToolVariable("keep-flagged-apis", "keep-flagged-apis")
+	pctx.HostBinToolVariable("aconfig-to-metalava-flags", "aconfig-to-metalava-flags")
 }
 
 type javaBuilderFlags struct {
diff --git a/java/droidstubs.go b/java/droidstubs.go
index caad688..b3241cc 100644
--- a/java/droidstubs.go
+++ b/java/droidstubs.go
@@ -821,19 +821,17 @@
 		}
 	}
 
-	if len(aconfigFlagsPaths) == 0 {
-		// This argument should not be added for "everything" stubs
-		cmd.Flag("--revert-annotation android.annotation.FlaggedApi")
-		return
-	}
+	// If aconfigFlagsPaths is empty then it is still important to generate the
+	// Metalava flags config file, albeit with an empty set of flags, so that all
+	// flagged APIs will be reverted.
 
-	releasedFlaggedApisFile := android.PathForModuleOut(ctx, fmt.Sprintf("released-flagged-apis-%s.txt", stubsType.String()))
-	revertAnnotationsFile := android.PathForModuleOut(ctx, fmt.Sprintf("revert-annotations-%s.txt", stubsType.String()))
+	releasedFlagsFile := android.PathForModuleOut(ctx, fmt.Sprintf("released-flags-%s.pb", stubsType.String()))
+	metalavaFlagsConfigFile := android.PathForModuleOut(ctx, fmt.Sprintf("flags-config-%s.xml", stubsType.String()))
 
 	ctx.Build(pctx, android.BuildParams{
 		Rule:        gatherReleasedFlaggedApisRule,
 		Inputs:      aconfigFlagsPaths,
-		Output:      releasedFlaggedApisFile,
+		Output:      releasedFlagsFile,
 		Description: fmt.Sprintf("%s gather aconfig flags", stubsType),
 		Args: map[string]string{
 			"flags_path":  android.JoinPathsWithPrefix(aconfigFlagsPaths, "--cache "),
@@ -843,12 +841,12 @@
 
 	ctx.Build(pctx, android.BuildParams{
 		Rule:        generateMetalavaRevertAnnotationsRule,
-		Input:       releasedFlaggedApisFile,
-		Output:      revertAnnotationsFile,
-		Description: fmt.Sprintf("%s revert annotations", stubsType),
+		Input:       releasedFlagsFile,
+		Output:      metalavaFlagsConfigFile,
+		Description: fmt.Sprintf("%s metalava flags config", stubsType),
 	})
 
-	cmd.FlagWithInput("@", revertAnnotationsFile)
+	cmd.FlagWithInput("--config-file ", metalavaFlagsConfigFile)
 }
 
 func (d *Droidstubs) commonMetalavaStubCmd(ctx android.ModuleContext, rule *android.RuleBuilder,
diff --git a/java/droidstubs_test.go b/java/droidstubs_test.go
index dfdf877..1f9d223 100644
--- a/java/droidstubs_test.go
+++ b/java/droidstubs_test.go
@@ -410,12 +410,12 @@
 
 	m := result.ModuleForTests(t, "foo", "android_common")
 	android.AssertStringDoesContain(t, "foo generates revert annotations file",
-		strings.Join(m.AllOutputs(), ""), "revert-annotations-exportable.txt")
+		strings.Join(m.AllOutputs(), ""), "flags-config-exportable.xml")
 
 	// revert-annotations.txt passed to exportable stubs generation metalava command
 	manifest := m.Output("metalava_exportable.sbox.textproto")
 	cmdline := String(android.RuleBuilderSboxProtoForTests(t, result.TestContext, manifest).Commands[0].Command)
-	android.AssertStringDoesContain(t, "flagged api hide command not included", cmdline, "revert-annotations-exportable.txt")
+	android.AssertStringDoesContain(t, "flagged api hide command not included", cmdline, "flags-config-exportable.xml")
 
 	android.AssertStringDoesContain(t, "foo generates exportable stubs jar",
 		strings.Join(m.AllOutputs(), ""), "exportable/foo-stubs.srcjar")
@@ -460,7 +460,7 @@
 
 	m := result.ModuleForTests(t, "foo", "android_common")
 
-	rule := m.Output("released-flagged-apis-exportable.txt")
+	rule := m.Output("released-flags-exportable.pb")
 	exposeWritableApisFilter := "--filter='state:ENABLED+permission:READ_ONLY' --filter='permission:READ_WRITE'"
 	android.AssertStringEquals(t, "Filter argument expected to contain READ_WRITE permissions", exposeWritableApisFilter, rule.Args["filter_args"])
 }
diff --git a/java/java_test.go b/java/java_test.go
index a6290a6..6df81e5 100644
--- a/java/java_test.go
+++ b/java/java_test.go
@@ -2912,12 +2912,12 @@
 
 	m := result.ModuleForTests(t, "foo", "android_common")
 	android.AssertStringDoesContain(t, "foo generates revert annotations file",
-		strings.Join(m.AllOutputs(), ""), "revert-annotations-exportable.txt")
+		strings.Join(m.AllOutputs(), ""), "flags-config-exportable.xml")
 
 	// revert-annotations.txt passed to exportable stubs generation metalava command
 	manifest := m.Output("metalava.sbox.textproto")
 	cmdline := String(android.RuleBuilderSboxProtoForTests(t, result.TestContext, manifest).Commands[0].Command)
-	android.AssertStringDoesContain(t, "flagged api hide command not included", cmdline, "revert-annotations-exportable.txt")
+	android.AssertStringDoesContain(t, "flagged api hide command not included", cmdline, "flags-config-exportable.xml")
 }
 
 func TestTestOnly(t *testing.T) {
diff --git a/scripts/Android.bp b/scripts/Android.bp
index 94163a5..c0e13d5 100644
--- a/scripts/Android.bp
+++ b/scripts/Android.bp
@@ -287,9 +287,13 @@
     ],
 }
 
-sh_binary_host {
-    name: "keep-flagged-apis",
-    src: "keep-flagged-apis.sh",
+python_binary_host {
+    name: "aconfig-to-metalava-flags",
+    main: "aconfig-to-metalava-flags.py",
+    srcs: ["aconfig-to-metalava-flags.py"],
+    libs: [
+        "libaconfig_python_proto",
+    ],
 }
 
 python_binary_host {
diff --git a/scripts/aconfig-to-metalava-flags.py b/scripts/aconfig-to-metalava-flags.py
new file mode 100644
index 0000000..efa85ec
--- /dev/null
+++ b/scripts/aconfig-to-metalava-flags.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2025 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.
+
+"""Converts a set of aconfig protobuf flags to a Metalava config file."""
+
+# Formatted using `pyformat -i scripts/aconfig-to-metalava-flags.py`
+
+import argparse
+import sys
+import xml.etree.ElementTree as ET
+
+from protos import aconfig_pb2
+
+_READ_ONLY = aconfig_pb2.flag_permission.READ_ONLY
+_ENABLED = aconfig_pb2.flag_state.ENABLED
+_DISABLED = aconfig_pb2.flag_state.DISABLED
+
+# The namespace of the Metalava config file.
+CONFIG_NS = 'http://www.google.com/tools/metalava/config'
+
+
+def config_name(tag: str):
+  """Create a QName in the config namespace.
+
+  :param:tag the name of the entity in the config namespace.
+  """
+  return f'{{{CONFIG_NS}}}{tag}'
+
+
+def main():
+  """Program entry point."""
+  args_parser = argparse.ArgumentParser(
+      description='Generate Metalava flags config from aconfig protobuf',
+  )
+  args_parser.add_argument(
+      'input',
+      help='The path to the aconfig protobuf file',
+  )
+  args = args_parser.parse_args(sys.argv[1:])
+
+  # Read the parsed_flags from the protobuf file.
+  with open(args.input, 'rb') as f:
+    parsed_flags = aconfig_pb2.parsed_flags.FromString(f.read())
+
+  # Create the structure of the XML config file.
+  config = ET.Element(config_name('config'))
+  api_flags = ET.SubElement(config, config_name('api-flags'))
+  # Create an <api-flag> element for each parsed_flag.
+  for flag in parsed_flags.parsed_flag:
+    if flag.permission == _READ_ONLY:
+      # Ignore any read only disabled flags as Metalava assumes that as the
+      # default when an <api-flags/> element is provided so this reduces the
+      # size of the file.
+      if flag.state == _DISABLED:
+        continue
+      mutability = 'immutable'
+    else:
+      mutability = 'mutable'
+    if flag.state == _ENABLED:
+      status = 'enabled'
+    else:
+      status = 'disabled'
+    attributes = {
+        'package': flag.package,
+        'name': flag.name,
+        'mutability': mutability,
+        'status': status,
+    }
+    # Convert the attribute names into qualified names in, what will become, the
+    # default namespace for the XML file. This is needed to ensure that the
+    # attribute will be written in the XML file without a prefix, e.g.
+    # `name="flag_name"`. Without it, a namespace prefix, e.g. `ns1`, will be
+    # synthesized for the attribute when writing the XML file, i.e. it
+    # will be written as `ns1:name="flag_name"`. Strictly speaking, that is
+    # unnecessary as the "Namespaces in XML 1.0 (Third Edition)" specification
+    # says that unprefixed attribute names have no namespace.
+    qualified_attributes = {config_name(k): v for (k, v) in attributes.items()}
+    ET.SubElement(api_flags, config_name('api-flag'), qualified_attributes)
+
+  # Create a tree and add white space so it will pretty print when written out.
+  tree = ET.ElementTree(config)
+  ET.indent(tree)
+
+  # Write the tree using the config namespace as the default.
+  tree.write(sys.stdout, encoding='unicode', default_namespace=CONFIG_NS)
+  sys.stdout.close()
+
+
+if __name__ == '__main__':
+  main()
diff --git a/scripts/keep-flagged-apis.sh b/scripts/keep-flagged-apis.sh
deleted file mode 100755
index 48efb7a..0000000
--- a/scripts/keep-flagged-apis.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash -e
-#
-# Copyright 2023 Google Inc. All rights reserved.
-#
-# 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.
-
-# Convert a list of flags in the input file to a list of metalava options
-# that will keep the APIs for those flags will hiding all other flagged
-# APIs.
-
-FLAGS="$1"
-
-FLAGGED="android.annotation.FlaggedApi"
-
-# Convert the list of feature flags in the input file to Metalava options
-# of the form `--revert-annotation !android.annotation.FlaggedApi("<flag>")`
-# to prevent the annotated APIs from being hidden, i.e. include the annotated
-# APIs in the SDK snapshots.
-while read -r line; do
-  # Escape and quote the key for sed
-  escaped_line=$(echo "$line" | sed "s/'/\\\'/g; s/ /\\ /g")
-
-  echo "--revert-annotation '!$FLAGGED(\"$escaped_line\")'"
-done < "$FLAGS"
-
-# Revert all flagged APIs, unless listed above.
-echo "--revert-annotation $FLAGGED"