| /* |
| * Copyright (C) 2023 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. |
| */ |
| |
| use anyhow::{ensure, Result}; |
| use serde::Serialize; |
| use std::collections::{BTreeMap, BTreeSet}; |
| use std::path::PathBuf; |
| use tinytemplate::TinyTemplate; |
| |
| use crate::codegen::{self, get_flag_offset_in_storage_file, CodegenMode}; |
| use crate::commands::OutputFile; |
| use aconfig_protos::{ |
| ProtoFlagPermission, ProtoFlagState, ProtoFlagStorageBackend, ProtoParsedFlag, |
| }; |
| use convert_finalized_flags::{ApiLevel, FinalizedFlag, FinalizedFlagMap}; |
| use std::collections::HashMap; |
| |
| // Arguments to configure codegen for generate_java_code. |
| pub struct JavaCodegenConfig { |
| pub codegen_mode: CodegenMode, |
| pub flag_ids: HashMap<String, u16>, |
| pub package_fingerprint: u64, |
| pub single_exported_file: bool, |
| pub finalized_flags: FinalizedFlagMap, |
| } |
| |
| pub fn generate_java_code<I>( |
| package: &str, |
| parsed_flags_iter: I, |
| config: JavaCodegenConfig, |
| ) -> Result<Vec<OutputFile>> |
| where |
| I: Iterator<Item = ProtoParsedFlag>, |
| { |
| let mut use_device_config = false; |
| let mut use_aconfigd = false; |
| let flag_elements: Vec<FlagElement> = parsed_flags_iter |
| .map(|pf| { |
| use_device_config |= pf.metadata.storage() == ProtoFlagStorageBackend::DEVICE_CONFIG; |
| use_aconfigd |= pf.metadata.storage() == ProtoFlagStorageBackend::ACONFIGD; |
| ensure!( |
| !(use_device_config && use_aconfigd), |
| "Package {} cannot contain both device_config and new storage stored flags", |
| package |
| ); |
| |
| create_flag_element(package, &pf, config.flag_ids.clone(), &config.finalized_flags) |
| }) |
| .collect::<Result<Vec<FlagElement>>>()?; |
| let namespace_flags = gen_flags_by_namespace(&flag_elements); |
| let properties_set: BTreeSet<String> = |
| flag_elements.iter().map(|fe| format_property_name(&fe.device_config_namespace)).collect(); |
| let is_test_mode = config.codegen_mode == CodegenMode::Test; |
| let library_exported = config.codegen_mode == CodegenMode::Exported; |
| let runtime_lookup_required = |
| flag_elements.iter().any(|elem| elem.is_read_write) || library_exported; |
| let container = (flag_elements.first().expect("zero template flags").container).to_string(); |
| let is_platform_container = |
| matches!(container.as_str(), "system" | "system_ext" | "product" | "vendor"); |
| let context = Context { |
| flag_elements, |
| namespace_flags, |
| is_test_mode, |
| runtime_lookup_required, |
| properties_set, |
| package_name: package.to_string(), |
| library_exported, |
| container, |
| is_platform_container, |
| package_fingerprint: format!("0x{:X}L", config.package_fingerprint), |
| single_exported_file: config.single_exported_file, |
| use_device_config, |
| }; |
| let mut template = TinyTemplate::new(); |
| if library_exported && config.single_exported_file { |
| template.add_template( |
| "ExportedFlags.java", |
| include_str!("../../templates/ExportedFlags.java.template"), |
| )?; |
| } else { |
| template.add_template( |
| "CustomFeatureFlags.java", |
| include_str!("../../templates/CustomFeatureFlags.java.template"), |
| )?; |
| template.add_template( |
| "FakeFeatureFlagsImpl.java", |
| include_str!("../../templates/FakeFeatureFlagsImpl.java.template"), |
| )?; |
| } |
| template.add_template("Flags.java", include_str!("../../templates/Flags.java.template"))?; |
| add_feature_flags_impl_template(&context, &mut template)?; |
| template.add_template( |
| "FeatureFlags.java", |
| include_str!("../../templates/FeatureFlags.java.template"), |
| )?; |
| |
| let path: PathBuf = package.split('.').collect(); |
| let mut files = vec!["Flags.java", "FeatureFlags.java", "FeatureFlagsImpl.java"]; |
| if library_exported && config.single_exported_file { |
| files.push("ExportedFlags.java"); |
| } else { |
| files.push("CustomFeatureFlags.java"); |
| files.push("FakeFeatureFlagsImpl.java"); |
| } |
| files |
| .iter() |
| .map(|file| { |
| Ok(OutputFile { |
| contents: template.render(file, &context)?.into(), |
| path: path.join(file), |
| }) |
| }) |
| .collect::<Result<Vec<OutputFile>>>() |
| } |
| |
| fn gen_flags_by_namespace(flags: &[FlagElement]) -> Vec<NamespaceFlags> { |
| let mut namespace_to_flag: BTreeMap<String, Vec<FlagElement>> = BTreeMap::new(); |
| |
| for flag in flags { |
| match namespace_to_flag.get_mut(&flag.device_config_namespace) { |
| Some(flag_list) => flag_list.push(flag.clone()), |
| None => { |
| namespace_to_flag.insert(flag.device_config_namespace.clone(), vec![flag.clone()]); |
| } |
| } |
| } |
| |
| namespace_to_flag |
| .iter() |
| .map(|(namespace, flags)| NamespaceFlags { |
| namespace: namespace.to_string(), |
| flags: flags.clone(), |
| }) |
| .collect() |
| } |
| |
| #[derive(Serialize)] |
| struct Context { |
| pub flag_elements: Vec<FlagElement>, |
| pub namespace_flags: Vec<NamespaceFlags>, |
| pub is_test_mode: bool, |
| pub runtime_lookup_required: bool, |
| pub properties_set: BTreeSet<String>, |
| pub package_name: String, |
| pub library_exported: bool, |
| pub container: String, |
| pub is_platform_container: bool, |
| pub package_fingerprint: String, |
| pub single_exported_file: bool, |
| pub use_device_config: bool, |
| } |
| |
| #[derive(Serialize, Debug)] |
| struct NamespaceFlags { |
| pub namespace: String, |
| pub flags: Vec<FlagElement>, |
| } |
| |
| #[derive(Serialize, Clone, Debug)] |
| struct FlagElement { |
| pub container: String, |
| pub default_value: bool, |
| pub device_config_namespace: String, |
| pub device_config_flag: String, |
| pub flag_name: String, |
| pub flag_name_constant_suffix: String, |
| pub flag_offset: u16, |
| pub is_read_write: bool, |
| pub method_name: String, |
| pub properties: String, |
| pub finalized_sdk_present: bool, |
| pub finalized_sdk_check: String, |
| } |
| |
| fn create_flag_element( |
| package: &str, |
| pf: &ProtoParsedFlag, |
| flag_ids: HashMap<String, u16>, |
| finalized_flags: &FinalizedFlagMap, |
| ) -> Result<FlagElement> { |
| let device_config_flag = codegen::create_device_config_ident(package, pf.name()) |
| .expect("values checked at flag parse time"); |
| |
| // An empty map is provided if check_api_level is disabled. |
| let (finalized_sdk_present, finalized_sdk_value) = if !finalized_flags.is_empty() { |
| let finalized_sdk = finalized_flags.get_finalized_level(&FinalizedFlag { |
| flag_name: pf.name().to_string(), |
| package_name: package.to_string(), |
| }); |
| (finalized_sdk.is_some(), finalized_sdk.unwrap_or(ApiLevel(0))) |
| } else { |
| (false, ApiLevel(0)) |
| }; |
| let finalized_sdk_check = finalized_sdk_value.conditional(); |
| |
| Ok(FlagElement { |
| container: pf.container().to_string(), |
| default_value: pf.state() == ProtoFlagState::ENABLED, |
| device_config_namespace: pf.namespace().to_string(), |
| device_config_flag, |
| flag_name: pf.name().to_string(), |
| flag_name_constant_suffix: pf.name().to_ascii_uppercase(), |
| flag_offset: get_flag_offset_in_storage_file(&flag_ids, pf)?, |
| is_read_write: pf.permission() == ProtoFlagPermission::READ_WRITE, |
| method_name: format_java_method_name(pf.name()), |
| properties: format_property_name(pf.namespace()), |
| finalized_sdk_present, |
| finalized_sdk_check, |
| }) |
| } |
| |
| fn format_java_method_name(flag_name: &str) -> String { |
| let splits: Vec<&str> = flag_name.split('_').filter(|&word| !word.is_empty()).collect(); |
| if splits.len() == 1 { |
| let name = splits[0]; |
| name[0..1].to_ascii_lowercase() + &name[1..] |
| } else { |
| splits |
| .iter() |
| .enumerate() |
| .map(|(index, word)| { |
| if index == 0 { |
| word.to_ascii_lowercase() |
| } else { |
| word[0..1].to_ascii_uppercase() + &word[1..].to_ascii_lowercase() |
| } |
| }) |
| .collect::<Vec<String>>() |
| .join("") |
| } |
| } |
| |
| fn format_property_name(property_name: &str) -> String { |
| let name = format_java_method_name(property_name); |
| format!("mProperties{}{}", &name[0..1].to_ascii_uppercase(), &name[1..]) |
| } |
| |
| fn add_feature_flags_impl_template( |
| context: &Context, |
| template: &mut TinyTemplate, |
| ) -> Result<(), tinytemplate::error::Error> { |
| if context.is_test_mode { |
| // Test mode has its own template, so use regardless of any other settings. |
| template.add_template( |
| "FeatureFlagsImpl.java", |
| include_str!("../../templates/FeatureFlagsImpl.test_mode.java.template"), |
| )?; |
| return Ok(()); |
| } |
| |
| match context.library_exported { |
| // Exported library with new_exported enabled, use new storage exported template. |
| true => { |
| template.add_template( |
| "FeatureFlagsImpl.java", |
| include_str!("../../templates/FeatureFlagsImpl.exported.java.template"), |
| )?; |
| } |
| // New storage internal mode. |
| false => { |
| template.add_template( |
| "FeatureFlagsImpl.java", |
| include_str!("../../templates/FeatureFlagsImpl.new_storage.java.template"), |
| )?; |
| } |
| }; |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use convert_finalized_flags::ApiLevel; |
| |
| use super::*; |
| use crate::commands::assign_flag_ids; |
| use std::collections::HashMap; |
| |
| const EXPECTED_FEATUREFLAGS_COMMON_CONTENT: &str = r#" |
| package com.android.aconfig.test; |
| // TODO(b/303773055): Remove the annotation after access issue is resolved. |
| import android.compat.annotation.UnsupportedAppUsage; |
| /** @hide */ |
| public interface FeatureFlags { |
| @com.android.aconfig.annotations.AssumeFalseForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean disabledRo(); |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean disabledRw(); |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean disabledRwExported(); |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean disabledRwInOtherNamespace(); |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean enabledFixedRo(); |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean enabledFixedRoExported(); |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean enabledRo(); |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean enabledRoExported(); |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean enabledRw(); |
| } |
| "#; |
| |
| const EXPECTED_FLAG_COMMON_CONTENT: &str = r#" |
| package com.android.aconfig.test; |
| // TODO(b/303773055): Remove the annotation after access issue is resolved. |
| import android.compat.annotation.UnsupportedAppUsage; |
| /** @hide */ |
| public final class Flags { |
| /** @hide */ |
| public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro"; |
| /** @hide */ |
| public static final String FLAG_DISABLED_RW = "com.android.aconfig.test.disabled_rw"; |
| /** @hide */ |
| public static final String FLAG_DISABLED_RW_EXPORTED = "com.android.aconfig.test.disabled_rw_exported"; |
| /** @hide */ |
| public static final String FLAG_DISABLED_RW_IN_OTHER_NAMESPACE = "com.android.aconfig.test.disabled_rw_in_other_namespace"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_FIXED_RO = "com.android.aconfig.test.enabled_fixed_ro"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_RW = "com.android.aconfig.test.enabled_rw"; |
| |
| @com.android.aconfig.annotations.AssumeFalseForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean disabledRo() { |
| return FEATURE_FLAGS.disabledRo(); |
| } |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean disabledRw() { |
| return FEATURE_FLAGS.disabledRw(); |
| } |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean disabledRwExported() { |
| return FEATURE_FLAGS.disabledRwExported(); |
| } |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean disabledRwInOtherNamespace() { |
| return FEATURE_FLAGS.disabledRwInOtherNamespace(); |
| } |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean enabledFixedRo() { |
| return FEATURE_FLAGS.enabledFixedRo(); |
| } |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean enabledFixedRoExported() { |
| return FEATURE_FLAGS.enabledFixedRoExported(); |
| } |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean enabledRo() { |
| return FEATURE_FLAGS.enabledRo(); |
| } |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean enabledRoExported() { |
| return FEATURE_FLAGS.enabledRoExported(); |
| } |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean enabledRw() { |
| return FEATURE_FLAGS.enabledRw(); |
| } |
| "#; |
| |
| const EXPECTED_CUSTOMFEATUREFLAGS_CONTENT: &str = r#" |
| package com.android.aconfig.test; |
| |
| // TODO(b/303773055): Remove the annotation after access issue is resolved. |
| import android.compat.annotation.UnsupportedAppUsage; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.BiPredicate; |
| import java.util.function.Predicate; |
| |
| /** @hide */ |
| public class CustomFeatureFlags implements FeatureFlags { |
| |
| private BiPredicate<String, Predicate<FeatureFlags>> mGetValueImpl; |
| |
| public CustomFeatureFlags(BiPredicate<String, Predicate<FeatureFlags>> getValueImpl) { |
| mGetValueImpl = getValueImpl; |
| } |
| |
| @Override |
| @UnsupportedAppUsage |
| public boolean disabledRo() { |
| return getValue(Flags.FLAG_DISABLED_RO, |
| FeatureFlags::disabledRo); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean disabledRw() { |
| return getValue(Flags.FLAG_DISABLED_RW, |
| FeatureFlags::disabledRw); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean disabledRwExported() { |
| return getValue(Flags.FLAG_DISABLED_RW_EXPORTED, |
| FeatureFlags::disabledRwExported); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean disabledRwInOtherNamespace() { |
| return getValue(Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE, |
| FeatureFlags::disabledRwInOtherNamespace); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean enabledFixedRo() { |
| return getValue(Flags.FLAG_ENABLED_FIXED_RO, |
| FeatureFlags::enabledFixedRo); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean enabledFixedRoExported() { |
| return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, |
| FeatureFlags::enabledFixedRoExported); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean enabledRo() { |
| return getValue(Flags.FLAG_ENABLED_RO, |
| FeatureFlags::enabledRo); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean enabledRoExported() { |
| return getValue(Flags.FLAG_ENABLED_RO_EXPORTED, |
| FeatureFlags::enabledRoExported); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean enabledRw() { |
| return getValue(Flags.FLAG_ENABLED_RW, |
| FeatureFlags::enabledRw); |
| } |
| |
| public boolean isFlagReadOnlyOptimized(String flagName) { |
| if (mReadOnlyFlagsSet.contains(flagName) && |
| isOptimizationEnabled()) { |
| return true; |
| } |
| return false; |
| } |
| |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| private boolean isOptimizationEnabled() { |
| return false; |
| } |
| |
| protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) { |
| return mGetValueImpl.test(flagName, getter); |
| } |
| |
| public List<String> getFlagNames() { |
| return Arrays.asList( |
| Flags.FLAG_DISABLED_RO, |
| Flags.FLAG_DISABLED_RW, |
| Flags.FLAG_DISABLED_RW_EXPORTED, |
| Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE, |
| Flags.FLAG_ENABLED_FIXED_RO, |
| Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, |
| Flags.FLAG_ENABLED_RO, |
| Flags.FLAG_ENABLED_RO_EXPORTED, |
| Flags.FLAG_ENABLED_RW |
| ); |
| } |
| |
| private Set<String> mReadOnlyFlagsSet = new HashSet<>( |
| Arrays.asList( |
| Flags.FLAG_DISABLED_RO, |
| Flags.FLAG_ENABLED_FIXED_RO, |
| Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, |
| Flags.FLAG_ENABLED_RO, |
| Flags.FLAG_ENABLED_RO_EXPORTED, |
| "" |
| ) |
| ); |
| } |
| "#; |
| |
| const EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT: &str = r#" |
| package com.android.aconfig.test; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.function.Predicate; |
| |
| /** @hide */ |
| public class FakeFeatureFlagsImpl extends CustomFeatureFlags { |
| private final Map<String, Boolean> mFlagMap = new HashMap<>(); |
| private final FeatureFlags mDefaults; |
| |
| public FakeFeatureFlagsImpl() { |
| this(null); |
| } |
| |
| public FakeFeatureFlagsImpl(FeatureFlags defaults) { |
| super(null); |
| mDefaults = defaults; |
| // Initialize the map with null values |
| for (String flagName : getFlagNames()) { |
| mFlagMap.put(flagName, null); |
| } |
| } |
| |
| @Override |
| protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) { |
| Boolean value = this.mFlagMap.get(flagName); |
| if (value != null) { |
| return value; |
| } |
| if (mDefaults != null) { |
| return getter.test(mDefaults); |
| } |
| throw new IllegalArgumentException(flagName + " is not set"); |
| } |
| |
| public void setFlag(String flagName, boolean value) { |
| if (!this.mFlagMap.containsKey(flagName)) { |
| throw new IllegalArgumentException("no such flag " + flagName); |
| } |
| this.mFlagMap.put(flagName, value); |
| } |
| |
| public void resetAll() { |
| for (Map.Entry entry : mFlagMap.entrySet()) { |
| entry.setValue(null); |
| } |
| } |
| } |
| "#; |
| |
| #[test] |
| fn test_generate_java_code_production() { |
| let parsed_flags = crate::test::parse_test_flags(); |
| let mode = CodegenMode::Production; |
| let modified_parsed_flags = |
| crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap(); |
| let flag_ids = |
| assign_flag_ids(crate::test::TEST_PACKAGE, modified_parsed_flags.iter()).unwrap(); |
| let config = JavaCodegenConfig { |
| codegen_mode: mode, |
| flag_ids, |
| package_fingerprint: 5801144784618221668, |
| single_exported_file: false, |
| finalized_flags: FinalizedFlagMap::new(), |
| }; |
| let generated_files = generate_java_code( |
| crate::test::TEST_PACKAGE, |
| modified_parsed_flags.into_iter(), |
| config, |
| ) |
| .unwrap(); |
| let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string() |
| + r#" |
| private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); |
| }"#; |
| |
| let expected_featureflagsmpl_content = r#" |
| package com.android.aconfig.test; |
| // TODO(b/303773055): Remove the annotation after access issue is resolved. |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.os.flagging.PlatformAconfigPackageInternal; |
| import android.util.Log; |
| /** @hide */ |
| public final class FeatureFlagsImpl implements FeatureFlags { |
| private static final String TAG = "FeatureFlagsImpl"; |
| private static volatile boolean isCached = false; |
| private static boolean disabledRw = false; |
| private static boolean disabledRwExported = false; |
| private static boolean disabledRwInOtherNamespace = false; |
| private static boolean enabledRw = true; |
| private void init() { |
| try { |
| PlatformAconfigPackageInternal reader = PlatformAconfigPackageInternal.load("com.android.aconfig.test", 0x5081CE7221C77064L); |
| disabledRw = reader.getBooleanFlagValue(0); |
| disabledRwExported = reader.getBooleanFlagValue(1); |
| enabledRw = reader.getBooleanFlagValue(7); |
| disabledRwInOtherNamespace = reader.getBooleanFlagValue(2); |
| } catch (Exception e) { |
| Log.e(TAG, e.toString()); |
| } catch (LinkageError e) { |
| // for mainline module running on older devices. |
| // This should be replaces to version check, after the version bump. |
| Log.e(TAG, e.toString()); |
| } |
| isCached = true; |
| } |
| |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean disabledRo() { |
| return false; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean disabledRw() { |
| if (!isCached) { |
| init(); |
| } |
| return disabledRw; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean disabledRwExported() { |
| if (!isCached) { |
| init(); |
| } |
| return disabledRwExported; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean disabledRwInOtherNamespace() { |
| if (!isCached) { |
| init(); |
| } |
| return disabledRwInOtherNamespace; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean enabledFixedRo() { |
| return true; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean enabledFixedRoExported() { |
| return true; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean enabledRo() { |
| return true; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean enabledRoExported() { |
| return true; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean enabledRw() { |
| if (!isCached) { |
| init(); |
| } |
| return enabledRw; |
| } |
| } |
| "#; |
| |
| let mut file_set = HashMap::from([ |
| ("com/android/aconfig/test/Flags.java", expect_flags_content.as_str()), |
| ("com/android/aconfig/test/FeatureFlagsImpl.java", expected_featureflagsmpl_content), |
| ("com/android/aconfig/test/FeatureFlags.java", EXPECTED_FEATUREFLAGS_COMMON_CONTENT), |
| ( |
| "com/android/aconfig/test/CustomFeatureFlags.java", |
| EXPECTED_CUSTOMFEATUREFLAGS_CONTENT, |
| ), |
| ( |
| "com/android/aconfig/test/FakeFeatureFlagsImpl.java", |
| EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT, |
| ), |
| ]); |
| |
| for file in generated_files { |
| let file_path = file.path.to_str().unwrap(); |
| assert!(file_set.contains_key(file_path), "Cannot find {}", file_path); |
| assert_eq!( |
| None, |
| crate::test::first_significant_code_diff( |
| file_set.get(file_path).unwrap(), |
| &String::from_utf8(file.contents).unwrap() |
| ), |
| "File {} content is not correct", |
| file_path |
| ); |
| file_set.remove(file_path); |
| } |
| |
| assert!(file_set.is_empty()); |
| } |
| |
| #[test] |
| fn test_generate_java_code_new_exported() { |
| let parsed_flags = crate::test::parse_test_flags(); |
| let mode = CodegenMode::Exported; |
| let modified_parsed_flags = |
| crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap(); |
| let flag_ids = |
| assign_flag_ids(crate::test::TEST_PACKAGE, modified_parsed_flags.iter()).unwrap(); |
| let config = JavaCodegenConfig { |
| codegen_mode: mode, |
| flag_ids, |
| package_fingerprint: 5801144784618221668, |
| single_exported_file: false, |
| finalized_flags: FinalizedFlagMap::new(), |
| }; |
| let generated_files = generate_java_code( |
| crate::test::TEST_PACKAGE, |
| modified_parsed_flags.into_iter(), |
| config, |
| ) |
| .unwrap(); |
| |
| let expect_flags_content = r#" |
| package com.android.aconfig.test; |
| import android.os.Build; |
| /** @hide */ |
| public final class Flags { |
| /** @hide */ |
| public static final String FLAG_DISABLED_RW_EXPORTED = "com.android.aconfig.test.disabled_rw_exported"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported"; |
| public static boolean disabledRwExported() { |
| return FEATURE_FLAGS.disabledRwExported(); |
| } |
| public static boolean enabledFixedRoExported() { |
| return FEATURE_FLAGS.enabledFixedRoExported(); |
| } |
| public static boolean enabledRoExported() { |
| return FEATURE_FLAGS.enabledRoExported(); |
| } |
| private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); |
| } |
| "#; |
| |
| let expect_feature_flags_content = r#" |
| package com.android.aconfig.test; |
| /** @hide */ |
| public interface FeatureFlags { |
| boolean disabledRwExported(); |
| boolean enabledFixedRoExported(); |
| boolean enabledRoExported(); |
| } |
| "#; |
| |
| let expect_feature_flags_impl_content = r#" |
| package com.android.aconfig.test; |
| import android.os.Build; |
| import android.os.flagging.AconfigPackage; |
| import android.util.Log; |
| /** @hide */ |
| public final class FeatureFlagsImpl implements FeatureFlags { |
| private static final String TAG = "FeatureFlagsImplExport"; |
| private static volatile boolean isCached = false; |
| private static boolean disabledRwExported = false; |
| private static boolean enabledFixedRoExported = false; |
| private static boolean enabledRoExported = false; |
| private void init() { |
| try { |
| AconfigPackage reader = AconfigPackage.load("com.android.aconfig.test"); |
| disabledRwExported = reader.getBooleanFlagValue("disabled_rw_exported", false); |
| enabledFixedRoExported = reader.getBooleanFlagValue("enabled_fixed_ro_exported", false); |
| enabledRoExported = reader.getBooleanFlagValue("enabled_ro_exported", false); |
| } catch (Exception e) { |
| // pass |
| Log.e(TAG, e.toString()); |
| } catch (LinkageError e) { |
| // for mainline module running on older devices. |
| // This should be replaces to version check, after the version bump. |
| Log.w(TAG, e.toString()); |
| } |
| isCached = true; |
| } |
| @Override |
| public boolean disabledRwExported() { |
| if (!isCached) { |
| init(); |
| } |
| return disabledRwExported; |
| } |
| @Override |
| public boolean enabledFixedRoExported() { |
| if (!isCached) { |
| init(); |
| } |
| return enabledFixedRoExported; |
| } |
| @Override |
| public boolean enabledRoExported() { |
| if (!isCached) { |
| init(); |
| } |
| return enabledRoExported; |
| } |
| }"#; |
| |
| let expect_custom_feature_flags_content = r#" |
| package com.android.aconfig.test; |
| |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.BiPredicate; |
| import java.util.function.Predicate; |
| import android.os.Build; |
| |
| /** @hide */ |
| public class CustomFeatureFlags implements FeatureFlags { |
| |
| private BiPredicate<String, Predicate<FeatureFlags>> mGetValueImpl; |
| |
| public CustomFeatureFlags(BiPredicate<String, Predicate<FeatureFlags>> getValueImpl) { |
| mGetValueImpl = getValueImpl; |
| } |
| |
| @Override |
| public boolean disabledRwExported() { |
| return getValue(Flags.FLAG_DISABLED_RW_EXPORTED, |
| FeatureFlags::disabledRwExported); |
| } |
| @Override |
| public boolean enabledFixedRoExported() { |
| return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, |
| FeatureFlags::enabledFixedRoExported); |
| } |
| @Override |
| public boolean enabledRoExported() { |
| return getValue(Flags.FLAG_ENABLED_RO_EXPORTED, |
| FeatureFlags::enabledRoExported); |
| } |
| |
| protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) { |
| return mGetValueImpl.test(flagName, getter); |
| } |
| |
| public List<String> getFlagNames() { |
| return Arrays.asList( |
| Flags.FLAG_DISABLED_RW_EXPORTED, |
| Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, |
| Flags.FLAG_ENABLED_RO_EXPORTED |
| ); |
| } |
| |
| private Set<String> mReadOnlyFlagsSet = new HashSet<>( |
| Arrays.asList( |
| "" |
| ) |
| ); |
| |
| private Map<String, Boolean> mFinalizedFlags = new HashMap<>( |
| Map.ofEntries( |
| Map.entry("", false) |
| ) |
| ); |
| |
| public boolean isFlagFinalized(String flagName) { |
| if (!mFinalizedFlags.containsKey(flagName)) { |
| return false; |
| } |
| return mFinalizedFlags.get(flagName); |
| } |
| } |
| "#; |
| |
| let mut file_set = HashMap::from([ |
| ("com/android/aconfig/test/Flags.java", expect_flags_content), |
| ("com/android/aconfig/test/FeatureFlags.java", expect_feature_flags_content), |
| ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_feature_flags_impl_content), |
| ( |
| "com/android/aconfig/test/CustomFeatureFlags.java", |
| expect_custom_feature_flags_content, |
| ), |
| ( |
| "com/android/aconfig/test/FakeFeatureFlagsImpl.java", |
| EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT, |
| ), |
| ]); |
| |
| for file in generated_files { |
| let file_path = file.path.to_str().unwrap(); |
| assert!(file_set.contains_key(file_path), "Cannot find {}", file_path); |
| assert_eq!( |
| None, |
| crate::test::first_significant_code_diff( |
| file_set.get(file_path).unwrap(), |
| &String::from_utf8(file.contents).unwrap() |
| ), |
| "File {} content is not correct", |
| file_path |
| ); |
| file_set.remove(file_path); |
| } |
| |
| assert!(file_set.is_empty()); |
| } |
| |
| #[test] |
| fn test_generate_java_code_new_exported_with_sdk_check() { |
| let parsed_flags = crate::test::parse_test_flags(); |
| let mode = CodegenMode::Exported; |
| let modified_parsed_flags = |
| crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap(); |
| let flag_ids = |
| assign_flag_ids(crate::test::TEST_PACKAGE, modified_parsed_flags.iter()).unwrap(); |
| let mut finalized_flags = FinalizedFlagMap::new(); |
| finalized_flags.insert_if_new( |
| ApiLevel::from_sdk_int(36), |
| FinalizedFlag { |
| flag_name: "disabled_rw_exported".to_string(), |
| package_name: "com.android.aconfig.test".to_string(), |
| }, |
| ); |
| let config = JavaCodegenConfig { |
| codegen_mode: mode, |
| flag_ids, |
| package_fingerprint: 5801144784618221668, |
| single_exported_file: false, |
| finalized_flags, |
| }; |
| let generated_files = generate_java_code( |
| crate::test::TEST_PACKAGE, |
| modified_parsed_flags.into_iter(), |
| config, |
| ) |
| .unwrap(); |
| |
| let expect_flags_content = r#" |
| package com.android.aconfig.test; |
| import android.os.Build; |
| /** @hide */ |
| public final class Flags { |
| /** @hide */ |
| public static final String FLAG_DISABLED_RW_EXPORTED = "com.android.aconfig.test.disabled_rw_exported"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported"; |
| public static boolean disabledRwExported() { |
| if (Build.VERSION.SDK_INT >= 36) { |
| return true; |
| } |
| return FEATURE_FLAGS.disabledRwExported(); |
| } |
| public static boolean enabledFixedRoExported() { |
| return FEATURE_FLAGS.enabledFixedRoExported(); |
| } |
| public static boolean enabledRoExported() { |
| return FEATURE_FLAGS.enabledRoExported(); |
| } |
| private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); |
| } |
| "#; |
| |
| let expect_feature_flags_content = r#" |
| package com.android.aconfig.test; |
| /** @hide */ |
| public interface FeatureFlags { |
| boolean disabledRwExported(); |
| boolean enabledFixedRoExported(); |
| boolean enabledRoExported(); |
| } |
| "#; |
| |
| let expect_feature_flags_impl_content = r#" |
| package com.android.aconfig.test; |
| import android.os.Build; |
| import android.os.flagging.AconfigPackage; |
| import android.util.Log; |
| /** @hide */ |
| public final class FeatureFlagsImpl implements FeatureFlags { |
| private static final String TAG = "FeatureFlagsImplExport"; |
| private static volatile boolean isCached = false; |
| private static boolean disabledRwExported = false; |
| private static boolean enabledFixedRoExported = false; |
| private static boolean enabledRoExported = false; |
| private void init() { |
| try { |
| AconfigPackage reader = AconfigPackage.load("com.android.aconfig.test"); |
| disabledRwExported = Build.VERSION.SDK_INT >= 36 ? true : reader.getBooleanFlagValue("disabled_rw_exported", false); |
| enabledFixedRoExported = reader.getBooleanFlagValue("enabled_fixed_ro_exported", false); |
| enabledRoExported = reader.getBooleanFlagValue("enabled_ro_exported", false); |
| } catch (Exception e) { |
| // pass |
| Log.e(TAG, e.toString()); |
| } catch (LinkageError e) { |
| // for mainline module running on older devices. |
| // This should be replaces to version check, after the version bump. |
| Log.w(TAG, e.toString()); |
| } |
| isCached = true; |
| } |
| @Override |
| public boolean disabledRwExported() { |
| if (!isCached) { |
| init(); |
| } |
| return disabledRwExported; |
| } |
| @Override |
| public boolean enabledFixedRoExported() { |
| if (!isCached) { |
| init(); |
| } |
| return enabledFixedRoExported; |
| } |
| @Override |
| public boolean enabledRoExported() { |
| if (!isCached) { |
| init(); |
| } |
| return enabledRoExported; |
| } |
| }"#; |
| |
| let expect_custom_feature_flags_content = r#" |
| package com.android.aconfig.test; |
| |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.BiPredicate; |
| import java.util.function.Predicate; |
| import android.os.Build; |
| |
| /** @hide */ |
| public class CustomFeatureFlags implements FeatureFlags { |
| |
| private BiPredicate<String, Predicate<FeatureFlags>> mGetValueImpl; |
| |
| public CustomFeatureFlags(BiPredicate<String, Predicate<FeatureFlags>> getValueImpl) { |
| mGetValueImpl = getValueImpl; |
| } |
| |
| @Override |
| public boolean disabledRwExported() { |
| return getValue(Flags.FLAG_DISABLED_RW_EXPORTED, |
| FeatureFlags::disabledRwExported); |
| } |
| @Override |
| public boolean enabledFixedRoExported() { |
| return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, |
| FeatureFlags::enabledFixedRoExported); |
| } |
| @Override |
| public boolean enabledRoExported() { |
| return getValue(Flags.FLAG_ENABLED_RO_EXPORTED, |
| FeatureFlags::enabledRoExported); |
| } |
| |
| protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) { |
| return mGetValueImpl.test(flagName, getter); |
| } |
| |
| public List<String> getFlagNames() { |
| return Arrays.asList( |
| Flags.FLAG_DISABLED_RW_EXPORTED, |
| Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, |
| Flags.FLAG_ENABLED_RO_EXPORTED |
| ); |
| } |
| |
| private Set<String> mReadOnlyFlagsSet = new HashSet<>( |
| Arrays.asList( |
| "" |
| ) |
| ); |
| |
| private Map<String, Boolean> mFinalizedFlags = new HashMap<>( |
| Map.ofEntries( |
| Map.entry(Flags.FLAG_DISABLED_RW_EXPORTED, Build.VERSION.SDK_INT >= 36 ? true : false), |
| Map.entry("", false) |
| ) |
| ); |
| |
| public boolean isFlagFinalized(String flagName) { |
| if (!mFinalizedFlags.containsKey(flagName)) { |
| return false; |
| } |
| return mFinalizedFlags.get(flagName); |
| } |
| } |
| "#; |
| |
| let mut file_set = HashMap::from([ |
| ("com/android/aconfig/test/Flags.java", expect_flags_content), |
| ("com/android/aconfig/test/FeatureFlags.java", expect_feature_flags_content), |
| ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_feature_flags_impl_content), |
| ( |
| "com/android/aconfig/test/CustomFeatureFlags.java", |
| expect_custom_feature_flags_content, |
| ), |
| ( |
| "com/android/aconfig/test/FakeFeatureFlagsImpl.java", |
| EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT, |
| ), |
| ]); |
| |
| for file in generated_files { |
| let file_path = file.path.to_str().unwrap(); |
| assert!(file_set.contains_key(file_path), "Cannot find {}", file_path); |
| assert_eq!( |
| None, |
| crate::test::first_significant_code_diff( |
| file_set.get(file_path).unwrap(), |
| &String::from_utf8(file.contents).unwrap() |
| ), |
| "File {} content is not correct", |
| file_path |
| ); |
| file_set.remove(file_path); |
| } |
| |
| assert!(file_set.is_empty()); |
| } |
| |
| // Test that the SDK check isn't added unless the library is exported (even |
| // if the flag is present in finalized_flags). |
| #[test] |
| fn test_generate_java_code_flags_with_sdk_check() { |
| let parsed_flags = crate::test::parse_test_flags(); |
| let mode = CodegenMode::Production; |
| let modified_parsed_flags = |
| crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap(); |
| let flag_ids = |
| assign_flag_ids(crate::test::TEST_PACKAGE, modified_parsed_flags.iter()).unwrap(); |
| let mut finalized_flags = FinalizedFlagMap::new(); |
| finalized_flags.insert_if_new( |
| ApiLevel::from_sdk_int(36), |
| FinalizedFlag { |
| flag_name: "disabled_rw".to_string(), |
| package_name: "com.android.aconfig.test".to_string(), |
| }, |
| ); |
| let config = JavaCodegenConfig { |
| codegen_mode: mode, |
| flag_ids, |
| package_fingerprint: 5801144784618221668, |
| single_exported_file: false, |
| finalized_flags, |
| }; |
| let generated_files = generate_java_code( |
| crate::test::TEST_PACKAGE, |
| modified_parsed_flags.into_iter(), |
| config, |
| ) |
| .unwrap(); |
| |
| let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string() |
| + r#" |
| private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); |
| }"#; |
| |
| let file = generated_files.iter().find(|f| f.path.ends_with("Flags.java")).unwrap(); |
| assert_eq!( |
| None, |
| crate::test::first_significant_code_diff( |
| &expect_flags_content, |
| &String::from_utf8(file.contents.clone()).unwrap() |
| ), |
| "Flags content is not correct" |
| ); |
| } |
| |
| #[test] |
| fn test_generate_java_code_test() { |
| let parsed_flags = crate::test::parse_test_flags(); |
| let mode = CodegenMode::Test; |
| let modified_parsed_flags = |
| crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap(); |
| let flag_ids = |
| assign_flag_ids(crate::test::TEST_PACKAGE, modified_parsed_flags.iter()).unwrap(); |
| let config = JavaCodegenConfig { |
| codegen_mode: mode, |
| flag_ids, |
| package_fingerprint: 5801144784618221668, |
| single_exported_file: false, |
| finalized_flags: FinalizedFlagMap::new(), |
| }; |
| let generated_files = generate_java_code( |
| crate::test::TEST_PACKAGE, |
| modified_parsed_flags.into_iter(), |
| config, |
| ) |
| .unwrap(); |
| |
| let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string() |
| + r#" |
| public static void setFeatureFlags(FeatureFlags featureFlags) { |
| Flags.FEATURE_FLAGS = featureFlags; |
| } |
| public static void unsetFeatureFlags() { |
| Flags.FEATURE_FLAGS = null; |
| } |
| private static FeatureFlags FEATURE_FLAGS; |
| } |
| "#; |
| let expect_featureflagsimpl_content = r#" |
| package com.android.aconfig.test; |
| /** @hide */ |
| public final class FeatureFlagsImpl implements FeatureFlags { |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean disabledRo() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean disabledRw() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean disabledRwExported() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean disabledRwInOtherNamespace() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean enabledFixedRo() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean enabledFixedRoExported() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean enabledRo() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean enabledRoExported() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| public boolean enabledRw() { |
| throw new UnsupportedOperationException( |
| "Method is not implemented."); |
| } |
| } |
| "#; |
| |
| let mut file_set = HashMap::from([ |
| ("com/android/aconfig/test/Flags.java", expect_flags_content.as_str()), |
| ("com/android/aconfig/test/FeatureFlags.java", EXPECTED_FEATUREFLAGS_COMMON_CONTENT), |
| ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_featureflagsimpl_content), |
| ( |
| "com/android/aconfig/test/CustomFeatureFlags.java", |
| EXPECTED_CUSTOMFEATUREFLAGS_CONTENT, |
| ), |
| ( |
| "com/android/aconfig/test/FakeFeatureFlagsImpl.java", |
| EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT, |
| ), |
| ]); |
| |
| for file in generated_files { |
| let file_path = file.path.to_str().unwrap(); |
| assert!(file_set.contains_key(file_path), "Cannot find {}", file_path); |
| assert_eq!( |
| None, |
| crate::test::first_significant_code_diff( |
| file_set.get(file_path).unwrap(), |
| &String::from_utf8(file.contents).unwrap() |
| ), |
| "File {} content is not correct", |
| file_path |
| ); |
| file_set.remove(file_path); |
| } |
| |
| assert!(file_set.is_empty()); |
| } |
| |
| #[test] |
| fn test_generate_java_code_force_read_only() { |
| let parsed_flags = crate::test::parse_test_flags(); |
| let mode = CodegenMode::ForceReadOnly; |
| let modified_parsed_flags = |
| crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap(); |
| let flag_ids = |
| assign_flag_ids(crate::test::TEST_PACKAGE, modified_parsed_flags.iter()).unwrap(); |
| let config = JavaCodegenConfig { |
| codegen_mode: mode, |
| flag_ids, |
| package_fingerprint: 5801144784618221668, |
| single_exported_file: false, |
| finalized_flags: FinalizedFlagMap::new(), |
| }; |
| let generated_files = generate_java_code( |
| crate::test::TEST_PACKAGE, |
| modified_parsed_flags.into_iter(), |
| config, |
| ) |
| .unwrap(); |
| let expect_featureflags_content = r#" |
| package com.android.aconfig.test; |
| // TODO(b/303773055): Remove the annotation after access issue is resolved. |
| import android.compat.annotation.UnsupportedAppUsage; |
| /** @hide */ |
| public interface FeatureFlags { |
| @com.android.aconfig.annotations.AssumeFalseForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean disabledRo(); |
| @com.android.aconfig.annotations.AssumeFalseForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean disabledRw(); |
| @com.android.aconfig.annotations.AssumeFalseForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean disabledRwInOtherNamespace(); |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean enabledFixedRo(); |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean enabledRo(); |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| boolean enabledRw(); |
| }"#; |
| |
| let expect_featureflagsimpl_content = r#" |
| package com.android.aconfig.test; |
| // TODO(b/303773055): Remove the annotation after access issue is resolved. |
| import android.compat.annotation.UnsupportedAppUsage; |
| /** @hide */ |
| public final class FeatureFlagsImpl implements FeatureFlags { |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean disabledRo() { |
| return false; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean disabledRw() { |
| return false; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean disabledRwInOtherNamespace() { |
| return false; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean enabledFixedRo() { |
| return true; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean enabledRo() { |
| return true; |
| } |
| @Override |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public boolean enabledRw() { |
| return true; |
| } |
| } |
| "#; |
| |
| let expect_flags_content = r#" |
| package com.android.aconfig.test; |
| // TODO(b/303773055): Remove the annotation after access issue is resolved. |
| import android.compat.annotation.UnsupportedAppUsage; |
| /** @hide */ |
| public final class Flags { |
| /** @hide */ |
| public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro"; |
| /** @hide */ |
| public static final String FLAG_DISABLED_RW = "com.android.aconfig.test.disabled_rw"; |
| /** @hide */ |
| public static final String FLAG_DISABLED_RW_IN_OTHER_NAMESPACE = "com.android.aconfig.test.disabled_rw_in_other_namespace"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_FIXED_RO = "com.android.aconfig.test.enabled_fixed_ro"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro"; |
| /** @hide */ |
| public static final String FLAG_ENABLED_RW = "com.android.aconfig.test.enabled_rw"; |
| @com.android.aconfig.annotations.AssumeFalseForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean disabledRo() { |
| return FEATURE_FLAGS.disabledRo(); |
| } |
| @com.android.aconfig.annotations.AssumeFalseForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean disabledRw() { |
| return FEATURE_FLAGS.disabledRw(); |
| } |
| @com.android.aconfig.annotations.AssumeFalseForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean disabledRwInOtherNamespace() { |
| return FEATURE_FLAGS.disabledRwInOtherNamespace(); |
| } |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean enabledFixedRo() { |
| return FEATURE_FLAGS.enabledFixedRo(); |
| } |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean enabledRo() { |
| return FEATURE_FLAGS.enabledRo(); |
| } |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| @com.android.aconfig.annotations.AconfigFlagAccessor |
| @UnsupportedAppUsage |
| public static boolean enabledRw() { |
| return FEATURE_FLAGS.enabledRw(); |
| } |
| private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); |
| }"#; |
| |
| let expect_customfeatureflags_content = r#" |
| package com.android.aconfig.test; |
| |
| // TODO(b/303773055): Remove the annotation after access issue is resolved. |
| import android.compat.annotation.UnsupportedAppUsage; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.BiPredicate; |
| import java.util.function.Predicate; |
| |
| /** @hide */ |
| public class CustomFeatureFlags implements FeatureFlags { |
| |
| private BiPredicate<String, Predicate<FeatureFlags>> mGetValueImpl; |
| |
| public CustomFeatureFlags(BiPredicate<String, Predicate<FeatureFlags>> getValueImpl) { |
| mGetValueImpl = getValueImpl; |
| } |
| |
| @Override |
| @UnsupportedAppUsage |
| public boolean disabledRo() { |
| return getValue(Flags.FLAG_DISABLED_RO, |
| FeatureFlags::disabledRo); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean disabledRw() { |
| return getValue(Flags.FLAG_DISABLED_RW, |
| FeatureFlags::disabledRw); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean disabledRwInOtherNamespace() { |
| return getValue(Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE, |
| FeatureFlags::disabledRwInOtherNamespace); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean enabledFixedRo() { |
| return getValue(Flags.FLAG_ENABLED_FIXED_RO, |
| FeatureFlags::enabledFixedRo); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean enabledRo() { |
| return getValue(Flags.FLAG_ENABLED_RO, |
| FeatureFlags::enabledRo); |
| } |
| @Override |
| @UnsupportedAppUsage |
| public boolean enabledRw() { |
| return getValue(Flags.FLAG_ENABLED_RW, |
| FeatureFlags::enabledRw); |
| } |
| |
| public boolean isFlagReadOnlyOptimized(String flagName) { |
| if (mReadOnlyFlagsSet.contains(flagName) && |
| isOptimizationEnabled()) { |
| return true; |
| } |
| return false; |
| } |
| |
| @com.android.aconfig.annotations.AssumeTrueForR8 |
| private boolean isOptimizationEnabled() { |
| return false; |
| } |
| |
| protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) { |
| return mGetValueImpl.test(flagName, getter); |
| } |
| |
| public List<String> getFlagNames() { |
| return Arrays.asList( |
| Flags.FLAG_DISABLED_RO, |
| Flags.FLAG_DISABLED_RW, |
| Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE, |
| Flags.FLAG_ENABLED_FIXED_RO, |
| Flags.FLAG_ENABLED_RO, |
| Flags.FLAG_ENABLED_RW |
| ); |
| } |
| |
| private Set<String> mReadOnlyFlagsSet = new HashSet<>( |
| Arrays.asList( |
| Flags.FLAG_DISABLED_RO, |
| Flags.FLAG_DISABLED_RW, |
| Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE, |
| Flags.FLAG_ENABLED_FIXED_RO, |
| Flags.FLAG_ENABLED_RO, |
| Flags.FLAG_ENABLED_RW, |
| "" |
| ) |
| ); |
| } |
| "#; |
| |
| let mut file_set = HashMap::from([ |
| ("com/android/aconfig/test/Flags.java", expect_flags_content), |
| ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_featureflagsimpl_content), |
| ("com/android/aconfig/test/FeatureFlags.java", expect_featureflags_content), |
| ("com/android/aconfig/test/CustomFeatureFlags.java", expect_customfeatureflags_content), |
| ( |
| "com/android/aconfig/test/FakeFeatureFlagsImpl.java", |
| EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT, |
| ), |
| ]); |
| |
| for file in generated_files { |
| let file_path = file.path.to_str().unwrap(); |
| assert!(file_set.contains_key(file_path), "Cannot find {}", file_path); |
| assert_eq!( |
| None, |
| crate::test::first_significant_code_diff( |
| file_set.get(file_path).unwrap(), |
| &String::from_utf8(file.contents).unwrap() |
| ), |
| "File {} content is not correct", |
| file_path |
| ); |
| file_set.remove(file_path); |
| } |
| |
| assert!(file_set.is_empty()); |
| } |
| |
| #[test] |
| fn test_generate_java_code_exported_flags() { |
| let parsed_flags = crate::test::parse_test_flags(); |
| let mode = CodegenMode::Exported; |
| let modified_parsed_flags = |
| crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap(); |
| let flag_ids = |
| assign_flag_ids(crate::test::TEST_PACKAGE, modified_parsed_flags.iter()).unwrap(); |
| let mut finalized_flags = FinalizedFlagMap::new(); |
| finalized_flags.insert_if_new( |
| ApiLevel::from_sdk_int(36), |
| FinalizedFlag { |
| flag_name: "disabled_rw_exported".to_string(), |
| package_name: "com.android.aconfig.test".to_string(), |
| }, |
| ); |
| let config = JavaCodegenConfig { |
| codegen_mode: mode, |
| flag_ids, |
| package_fingerprint: 5801144784618221668, |
| single_exported_file: true, |
| finalized_flags, |
| }; |
| let generated_files = generate_java_code( |
| crate::test::TEST_PACKAGE, |
| modified_parsed_flags.into_iter(), |
| config, |
| ) |
| .unwrap(); |
| |
| let expect_exported_flags_content = r#" |
| package com.android.aconfig.test; |
| |
| import android.os.Build; |
| import android.os.flagging.AconfigPackage; |
| import android.util.Log; |
| public final class ExportedFlags { |
| |
| public static final String FLAG_DISABLED_RW_EXPORTED = "com.android.aconfig.test.disabled_rw_exported"; |
| public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported"; |
| public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported"; |
| private static final String TAG = "ExportedFlags"; |
| private static volatile boolean isCached = false; |
| |
| private static boolean disabledRwExported = false; |
| private static boolean enabledFixedRoExported = false; |
| private static boolean enabledRoExported = false; |
| private ExportedFlags() {} |
| |
| private void init() { |
| try { |
| AconfigPackage reader = AconfigPackage.load("com.android.aconfig.test"); |
| disabledRwExported = reader.getBooleanFlagValue("disabled_rw_exported", false); |
| enabledFixedRoExported = reader.getBooleanFlagValue("enabled_fixed_ro_exported", false); |
| enabledRoExported = reader.getBooleanFlagValue("enabled_ro_exported", false); |
| } catch (Exception e) { |
| // pass |
| Log.e(TAG, e.toString()); |
| } catch (LinkageError e) { |
| // for mainline module running on older devices. |
| // This should be replaces to version check, after the version bump. |
| Log.w(TAG, e.toString()); |
| } |
| isCached = true; |
| } |
| public static boolean disabledRwExported() { |
| if (Build.VERSION.SDK_INT >= 36) { |
| return true; |
| } |
| |
| if (!featureFlags.isCached) { |
| featureFlags.init(); |
| } |
| return featureFlags.disabledRwExported; |
| } |
| public static boolean enabledFixedRoExported() { |
| if (!featureFlags.isCached) { |
| featureFlags.init(); |
| } |
| return featureFlags.enabledFixedRoExported; |
| } |
| public static boolean enabledRoExported() { |
| if (!featureFlags.isCached) { |
| featureFlags.init(); |
| } |
| return featureFlags.enabledRoExported; |
| } |
| private static ExportedFlags featureFlags = new ExportedFlags(); |
| }"#; |
| |
| let file = generated_files.iter().find(|f| f.path.ends_with("ExportedFlags.java")).unwrap(); |
| assert_eq!(4, generated_files.len()); |
| assert_eq!( |
| None, |
| crate::test::first_significant_code_diff( |
| expect_exported_flags_content, |
| &String::from_utf8(file.contents.clone()).unwrap() |
| ), |
| "ExportedFlags content is not correct" |
| ); |
| } |
| |
| #[test] |
| fn test_mix_device_config_and_new_storage_flags() { |
| let mut parsed_flags = crate::test::parse_test_flags(); |
| parsed_flags.parsed_flag[0].set_permission(ProtoFlagPermission::READ_WRITE); |
| let m = parsed_flags.parsed_flag[0].metadata.as_mut().unwrap(); |
| m.set_storage(ProtoFlagStorageBackend::DEVICE_CONFIG); |
| parsed_flags.parsed_flag[1].set_permission(ProtoFlagPermission::READ_WRITE); |
| let m = parsed_flags.parsed_flag[1].metadata.as_mut().unwrap(); |
| m.set_storage(ProtoFlagStorageBackend::ACONFIGD); |
| |
| let flag_ids = |
| assign_flag_ids(crate::test::TEST_PACKAGE, parsed_flags.parsed_flag.iter()).unwrap(); |
| |
| let config = JavaCodegenConfig { |
| codegen_mode: CodegenMode::Production, |
| flag_ids, |
| package_fingerprint: 5801144784618221668, |
| single_exported_file: false, |
| finalized_flags: FinalizedFlagMap::new(), |
| }; |
| let error = generate_java_code( |
| crate::test::TEST_PACKAGE, |
| parsed_flags.parsed_flag.into_iter(), |
| config, |
| ) |
| .unwrap_err(); |
| assert_eq!( |
| format!("{:?}", error), |
| "Package com.android.aconfig.test cannot contain both device_config and new storage stored flags", |
| ); |
| } |
| |
| #[test] |
| fn test_format_java_method_name() { |
| let expected = "someSnakeName"; |
| let input = "____some_snake___name____"; |
| let formatted_name = format_java_method_name(input); |
| assert_eq!(expected, formatted_name); |
| |
| let input = "someSnakeName"; |
| let formatted_name = format_java_method_name(input); |
| assert_eq!(expected, formatted_name); |
| |
| let input = "SomeSnakeName"; |
| let formatted_name = format_java_method_name(input); |
| assert_eq!(expected, formatted_name); |
| |
| let input = "SomeSnakeName_"; |
| let formatted_name = format_java_method_name(input); |
| assert_eq!(expected, formatted_name); |
| |
| let input = "_SomeSnakeName"; |
| let formatted_name = format_java_method_name(input); |
| assert_eq!(expected, formatted_name); |
| } |
| |
| #[test] |
| fn test_format_property_name() { |
| let expected = "mPropertiesSomeSnakeName"; |
| let input = "____some_snake___name____"; |
| let formatted_name = format_property_name(input); |
| assert_eq!(expected, formatted_name); |
| |
| let input = "someSnakeName"; |
| let formatted_name = format_property_name(input); |
| assert_eq!(expected, formatted_name); |
| |
| let input = "SomeSnakeName"; |
| let formatted_name = format_property_name(input); |
| assert_eq!(expected, formatted_name); |
| |
| let input = "SomeSnakeName_"; |
| let formatted_name = format_property_name(input); |
| assert_eq!(expected, formatted_name); |
| } |
| } |