1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.platform.test.flag.junit;
18 
19 import android.annotation.SuppressLint;
20 import android.app.UiAutomation;
21 import android.platform.test.flag.util.Flag;
22 import android.platform.test.flag.util.FlagReadException;
23 import android.provider.DeviceConfig;
24 
25 import androidx.test.platform.app.InstrumentationRegistry;
26 
27 import com.google.common.base.CaseFormat;
28 
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 import java.util.Set;
32 
33 /** A {@code IFlagsValueProvider} which provides flag values from device side. */
34 public class DeviceFlagsValueProvider implements IFlagsValueProvider {
35     private static final String READ_DEVICE_CONFIG_PERMISSION =
36             "android.permission.READ_DEVICE_CONFIG";
37 
38     private static final Set<String> VALID_BOOLEAN_VALUE = Set.of("true", "false");
39 
40     private final UiAutomation mUiAutomation;
41 
createCheckFlagsRule()42     public static CheckFlagsRule createCheckFlagsRule() {
43         return new CheckFlagsRule(new DeviceFlagsValueProvider());
44     }
45 
46     /**
47      * Creates a {@link CheckFlagsRule} with an existing {@link UiAutomation} instance.
48      *
49      * <p>This is necessary if the test modifies {@link UiAutomation} flags.
50      *
51      * @param uiAutomation The {@link UiAutomation} used by the test.
52      */
createCheckFlagsRule(UiAutomation uiAutomation)53     public static CheckFlagsRule createCheckFlagsRule(UiAutomation uiAutomation) {
54         return new CheckFlagsRule(new DeviceFlagsValueProvider(uiAutomation));
55     }
56 
DeviceFlagsValueProvider()57     public DeviceFlagsValueProvider() {
58         mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
59     }
60 
DeviceFlagsValueProvider(UiAutomation uiAutomation)61     private DeviceFlagsValueProvider(UiAutomation uiAutomation) {
62         mUiAutomation = uiAutomation;
63     }
64 
65     @Override
setUp()66     public void setUp() {
67         mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG_PERMISSION);
68     }
69 
70     @Override
getBoolean(String flag)71     public boolean getBoolean(String flag) throws FlagReadException {
72         Flag parsedFlag = Flag.createFlag(flag);
73         if (parsedFlag.namespace() != null) {
74             if (parsedFlag.packageName() != null) {
75                 throw new FlagReadException(
76                         flag, "You can not specify the namespace for aconfig flags.");
77             }
78             return getLegacyFlagBoolean(parsedFlag);
79         }
80         if (parsedFlag.packageName() == null) {
81             throw new FlagReadException(
82                     flag, "Flag name is not the expected format {packageName}.{flagName}.");
83         }
84         String className = parsedFlag.flagsClassName();
85         String simpleFlagName = parsedFlag.simpleFlagName();
86 
87         // Must be consistent with method name in aconfig auto generated code.
88         String methodName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, simpleFlagName);
89 
90         try {
91             Class<?> flagsClass = Class.forName(className);
92             Method method = flagsClass.getMethod(methodName);
93             Object result = method.invoke(null);
94             if (result instanceof Boolean) {
95                 return (Boolean) result;
96             }
97             throw new FlagReadException(
98                     flag,
99                     String.format(
100                             "Flag type is %s, not boolean", result.getClass().getSimpleName()));
101         } catch (ClassNotFoundException e) {
102             throw new FlagReadException(
103                     flag,
104                     String.format(
105                             "Can not load the Flags class %s to get its values. Please check the "
106                                     + "flag name and ensure that the aconfig auto generated "
107                                     + "library is in the dependency.",
108                             className),
109                     e);
110         } catch (NoSuchMethodException e) {
111             throw new FlagReadException(
112                     flag,
113                     String.format(
114                             "No method %s in the Flags class to read the flag value. Please check"
115                                     + " the flag name.",
116                             methodName),
117                     e);
118         } catch (InvocationTargetException | IllegalAccessException e) {
119             throw new FlagReadException(flag, e);
120         }
121     }
122 
getLegacyFlagBoolean(Flag flag)123     private boolean getLegacyFlagBoolean(Flag flag) throws FlagReadException {
124         @SuppressLint("MissingPermission")
125         String property = DeviceConfig.getProperty(flag.namespace(), flag.fullFlagName());
126         if (property == null) {
127             throw new FlagReadException(flag.fullFlagName(), "Flag does not exist on the device.");
128         }
129         if (VALID_BOOLEAN_VALUE.contains(property)) {
130             return Boolean.valueOf(property);
131         }
132         throw new FlagReadException(
133                 flag.fullFlagName(), String.format("Value %s is not a valid boolean.", property));
134     }
135 
136     @Override
tearDownBeforeTest()137     public void tearDownBeforeTest() {
138         mUiAutomation.dropShellPermissionIdentity();
139     }
140 }
141