1 /* 2 * Copyright (C) 2021 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 com.android.bedstead.testapp; 18 19 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_ENCRYPTED_STORAGE; 20 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_DISABLE_CAMERA; 21 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES; 22 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD; 23 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_FORCE_LOCK; 24 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD; 25 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_RESET_PASSWORD; 26 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_SETS_GLOBAL_PROXY; 27 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_WATCH_LOGIN; 28 import static com.android.bedstead.nene.devicepolicy.CommonDeviceAdminInfo.USES_POLICY_WIPE_DATA; 29 30 import android.content.Context; 31 import android.content.IntentFilter; 32 import android.os.Bundle; 33 import android.util.Log; 34 35 import com.android.bedstead.nene.TestApis; 36 import com.android.queryable.annotations.Query; 37 import com.android.queryable.info.ActivityInfo; 38 import com.android.queryable.info.MetadataInfo; 39 import com.android.queryable.info.MetadataValue; 40 import com.android.queryable.info.ReceiverInfo; 41 import com.android.queryable.info.ResourceInfo; 42 import com.android.queryable.info.ServiceInfo; 43 44 import com.google.common.collect.ImmutableMap; 45 import com.google.common.io.ByteStreams; 46 47 import org.w3c.dom.Document; 48 import org.w3c.dom.Node; 49 import org.w3c.dom.NodeList; 50 import org.xml.sax.InputSource; 51 import org.xml.sax.SAXException; 52 53 import java.io.IOException; 54 import java.io.InputStream; 55 import java.io.StringReader; 56 import java.nio.charset.StandardCharsets; 57 import java.util.ArrayList; 58 import java.util.Collections; 59 import java.util.Comparator; 60 import java.util.HashSet; 61 import java.util.List; 62 import java.util.Map; 63 import java.util.Set; 64 65 import javax.xml.parsers.DocumentBuilder; 66 import javax.xml.parsers.DocumentBuilderFactory; 67 import javax.xml.parsers.ParserConfigurationException; 68 69 /** Entry point to Test App. Used for querying for {@link TestApp} instances. */ 70 public final class TestAppProvider { 71 72 private static final String TAG = TestAppProvider.class.getSimpleName(); 73 74 // Must be instrumentation context to access resources 75 private static final Context sContext = TestApis.context().instrumentationContext(); 76 private boolean mTestAppsInitialised = false; 77 private final List<TestAppDetails> mTestApps = new ArrayList<>(); 78 private Set<TestAppDetails> mTestAppsSnapshot = null; 79 80 private static final Map<String, Integer> sPoliciesIntToXmlTagMap = 81 ImmutableMap.of("limit-password", USES_POLICY_LIMIT_PASSWORD, 82 "watch-login", USES_POLICY_WATCH_LOGIN, 83 "reset-password", USES_POLICY_RESET_PASSWORD, 84 "force-lock", USES_POLICY_FORCE_LOCK, 85 "wipe-data", USES_POLICY_WIPE_DATA, 86 "set-global-proxy", USES_POLICY_SETS_GLOBAL_PROXY, 87 "expire-password", USES_POLICY_EXPIRE_PASSWORD, 88 "encrypted-storage", USES_ENCRYPTED_STORAGE, 89 "disable-camera", USES_POLICY_DISABLE_CAMERA, 90 "disable-keyguard-features", USES_POLICY_DISABLE_KEYGUARD_FEATURES); 91 TestAppProvider()92 public TestAppProvider() { 93 initTestApps(); 94 } 95 96 /** Begin a query for a {@link TestApp}. */ query()97 public TestAppQueryBuilder query() { 98 return new TestAppQueryBuilder(this); 99 } 100 101 /** Create a query for a {@link TestApp} starting with a {@link Query}. */ query(Query query)102 public TestAppQueryBuilder query(Query query) { 103 return query().applyAnnotation(query); 104 } 105 106 /** Get any {@link TestApp}. */ any()107 public TestApp any() { 108 TestApp testApp = query().get(); 109 Log.d(TAG, "any(): returning " + testApp); 110 return testApp; 111 } 112 testApps()113 List<TestAppDetails> testApps() { 114 return mTestApps; 115 } 116 117 /** Save the state of the provider, to be reset by {@link #restore()}. */ snapshot()118 public void snapshot() { 119 mTestAppsSnapshot = new HashSet<>(mTestApps); 120 } 121 122 /** 123 * Restore the state of the provider to that recorded by {@link #snapshot()}. 124 */ restore()125 public void restore() { 126 if (mTestAppsSnapshot == null) { 127 throw new IllegalStateException("You must call snapshot() before restore()"); 128 } 129 mTestApps.clear(); 130 mTestApps.addAll(mTestAppsSnapshot); 131 } 132 133 /** 134 * Release resources. 135 * <br><br> 136 * Note: This method is intended for internal use and should <b>not</b> be called outside core 137 * Bedstead infrastructure. 138 */ releaseResources()139 public void releaseResources() { 140 mTestApps.clear(); 141 mTestAppsSnapshot.clear(); 142 } 143 initTestApps()144 private void initTestApps() { 145 if (mTestAppsInitialised) { 146 return; 147 } 148 mTestAppsInitialised = true; 149 150 try (InputStream inputStream = sContext.getAssets().open("testapps/index.txt")) { 151 TestappProtos.TestAppIndex index = TestappProtos.TestAppIndex.parseFrom(inputStream); 152 for (int i = 0; i < index.getAppsCount(); i++) { 153 loadApk(index.getApps(i)); 154 } 155 Collections.sort(mTestApps, 156 Comparator.comparing((testAppDetails) -> testAppDetails.mApp.getPackageName())); 157 } catch (IOException e) { 158 throw new RuntimeException("Error loading testapp index", e); 159 } 160 } 161 loadApk(TestappProtos.AndroidApp app)162 private void loadApk(TestappProtos.AndroidApp app) throws IOException { 163 TestAppDetails details = new TestAppDetails(); 164 details.mApp = app; 165 166 for (int i = 0; i < app.getMetadataCount(); i++) { 167 TestappProtos.Metadata metadataEntry = app.getMetadata(i); 168 MetadataInfo metadataInfo = MetadataInfo.builder().key(metadataEntry.getName()) 169 .value(MetadataValue.builder().value(metadataEntry.getValue()).build()) 170 .build(); 171 172 if (!metadataEntry.getResource().isEmpty()) { 173 String resourceName = metadataEntry.getValue(); 174 if (!resourceName.isEmpty()) { 175 // TODO(b/273291850): enable parsing of non-xml resources as well. 176 try (InputStream inputStream = 177 sContext.getAssets().open("resources/" + resourceName + ".xml")) { 178 String content = 179 new String(ByteStreams.toByteArray(inputStream), StandardCharsets.UTF_8); 180 metadataInfo.setResource(ResourceInfo.builder().content(content).build()); 181 Set<Integer> policies = fetchPoliciesFromResource(content); 182 if (policies != null) { 183 details.mPolicies.addAll(policies); 184 } 185 } 186 } 187 } 188 189 details.mMetadata.add(metadataInfo); 190 } 191 192 for (int i = 0; i < app.getPermissionsCount(); i++) { 193 details.mPermissions.add(app.getPermissions(i).getName()); 194 } 195 196 for (int i = 0; i < app.getActivitiesCount(); i++) { 197 TestappProtos.Activity activityEntry = app.getActivities(i); 198 details.mActivities.add(ActivityInfo.builder() 199 .activityClass(activityEntry.getName()) 200 .exported(activityEntry.getExported()) 201 .intentFilters(intentFilterSetFromProtoList( 202 activityEntry.getIntentFiltersList())) 203 .permission(activityEntry.getPermission().equals("") ? null 204 : activityEntry.getPermission()) 205 .build()); 206 } 207 208 for (int i = 0; i < app.getActivityAliasesCount(); i++) { 209 TestappProtos.ActivityAlias activityAliasEntry = app.getActivityAliases(i); 210 ActivityInfo activityInfo = ActivityInfo.builder() 211 .activityClass(activityAliasEntry.getName()) 212 .exported(activityAliasEntry.getExported()) 213 .intentFilters(intentFilterSetFromProtoList( 214 activityAliasEntry.getIntentFiltersList())) 215 .permission(activityAliasEntry.getPermission().equals("") ? null 216 : activityAliasEntry.getPermission()) 217 .build(); 218 219 details.mActivityAliases.add(activityInfo); 220 221 } 222 223 for (int i = 0; i < app.getServicesCount(); i++) { 224 TestappProtos.Service serviceEntry = app.getServices(i); 225 details.mServices.add(ServiceInfo.builder() 226 .serviceClass(serviceEntry.getName()) 227 .intentFilters(intentFilterSetFromProtoList( 228 serviceEntry.getIntentFiltersList())) 229 .metadata(metadataSetFromProtoList( 230 serviceEntry.getMetadataList())) 231 .build()); 232 } 233 234 for (int i = 0; i < app.getReceiversCount(); i++) { 235 TestappProtos.Receiver receiverEntry = app.getReceivers(i); 236 details.mReceivers.add(ReceiverInfo.builder() 237 .name(receiverEntry.getName()) 238 .metadata(metadataSetFromProtoList(receiverEntry.getMetadataList())) 239 .build()); 240 } 241 242 mTestApps.add(details); 243 } 244 intentFilterSetFromProtoList( List<TestappProtos.IntentFilter> list)245 private Set<IntentFilter> intentFilterSetFromProtoList( 246 List<TestappProtos.IntentFilter> list) { 247 Set<IntentFilter> filterInfoSet = new HashSet<>(); 248 249 for (TestappProtos.IntentFilter filter : list) { 250 IntentFilter filterInfo = intentFilterFromProto(filter); 251 filterInfoSet.add(filterInfo); 252 } 253 254 return filterInfoSet; 255 } 256 intentFilterFromProto(TestappProtos.IntentFilter filterProto)257 private IntentFilter intentFilterFromProto(TestappProtos.IntentFilter filterProto) { 258 IntentFilter filter = new IntentFilter(); 259 260 for (String action : filterProto.getActionsList()) { 261 filter.addAction(action); 262 } 263 for (String category : filterProto.getCategoriesList()) { 264 filter.addCategory(category); 265 } 266 267 return filter; 268 } 269 metadataSetFromProtoList( List<TestappProtos.Metadata> list)270 private Set<Bundle> metadataSetFromProtoList( 271 List<TestappProtos.Metadata> list) { 272 Set<Bundle> metadataSet = new HashSet<>(); 273 274 for (TestappProtos.Metadata metadata : list) { 275 Bundle metadataBundle = new Bundle(); 276 metadataBundle.putString(metadata.getName(), metadata.getValue()); 277 metadataSet.add(metadataBundle); 278 } 279 280 return metadataSet; 281 } 282 markTestAppUsed(TestAppDetails testApp)283 void markTestAppUsed(TestAppDetails testApp) { 284 mTestApps.remove(testApp); 285 } 286 fetchPoliciesFromResource(String resourceContent)287 private Set<Integer> fetchPoliciesFromResource(String resourceContent) { 288 Document document = convertStringToXml(resourceContent); 289 NodeList nodeList = document.getElementsByTagName("uses-policies"); 290 if (nodeList == null || nodeList.item(0) == null) { 291 return null; 292 } 293 nodeList = document.getElementsByTagName("uses-policies").item(0) 294 .getChildNodes(); 295 296 Set<Integer> policies = new HashSet<>(); 297 for (int i = 0; i < nodeList.getLength(); i++) { 298 Node node = nodeList.item(i); 299 if (node.getNodeType() == Node.ELEMENT_NODE) { 300 Integer policyIntValue = sPoliciesIntToXmlTagMap.get(node.getNodeName()); 301 if (policyIntValue == null) { 302 continue; 303 } 304 policies.add(policyIntValue); 305 } 306 } 307 return policies; 308 } 309 convertStringToXml(String xmlString)310 private static Document convertStringToXml(String xmlString) { 311 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 312 try { 313 DocumentBuilder builder = dbf.newDocumentBuilder(); 314 return builder.parse(new InputSource(new StringReader(xmlString))); 315 } catch (ParserConfigurationException | IOException | SAXException e) { 316 throw new RuntimeException(e); 317 } 318 } 319 320 } 321