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