• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.interactive;
18 
19 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
20 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
21 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
22 
23 import static com.android.bedstead.nene.appops.CommonAppOps.OPSTR_MANAGE_EXTERNAL_STORAGE;
24 
25 import android.content.Context;
26 import android.net.Uri;
27 import android.os.CancellationSignal;
28 import android.os.ParcelFileDescriptor;
29 import android.util.Log;
30 
31 import androidx.test.platform.app.InstrumentationRegistry;
32 
33 import com.android.bedstead.nene.TestApis;
34 import com.android.bedstead.nene.appops.AppOpsMode;
35 import com.android.bedstead.nene.packages.Package;
36 import com.android.bedstead.nene.permissions.PermissionContext;
37 import com.android.bedstead.nene.users.UserReference;
38 import com.android.interactive.annotations.AutomationFor;
39 
40 import dalvik.system.DexClassLoader;
41 import dalvik.system.DexFile;
42 
43 import java.io.File;
44 import java.io.FileInputStream;
45 import java.io.FileOutputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
49 import java.lang.annotation.Annotation;
50 import java.lang.reflect.InvocationTargetException;
51 import java.util.Enumeration;
52 import java.util.HashMap;
53 import java.util.Map;
54 
55 /**
56  * Logic for loading an APK containing step automations and running those automations.
57  */
58 public final class Automator {
59 
60     private static final String LOG_TAG = "Interaction.Automator";
61 
62     private final Context mContext = TestApis.context().instrumentedContext();
63     private final String mAutomationFile;
64     private boolean mHasInitialised = false;
65     private Map<String, Automation<?>> mAutomationClasses = new HashMap<>();
66 
67     public static final String AUTOMATION_FILE = "/sdcard/InteractiveAutomation.apk";
68 
69     /**
70      * Create an {@link Automator} for the given automation APK.
71      */
Automator(String automationFile)72     public Automator(String automationFile) {
73         mAutomationFile = automationFile;
74     }
75 
copy(InputStream source, OutputStream target)76     private void copy(InputStream source, OutputStream target) throws IOException {
77         byte[] buf = new byte[1024];
78         int length;
79         while ((length = source.read(buf)) != -1) {
80             target.write(buf, 0, length);
81         }
82     }
83 
84     /**
85      * If we're not on the system user, we try to fetch the automation file from the system user.
86      */
copyAutomationFileToInternalStorage()87     private File copyAutomationFileToInternalStorage() {
88         try (PermissionContext p = TestApis.permissions()
89                 .withPermission(READ_EXTERNAL_STORAGE, MANAGE_EXTERNAL_STORAGE,
90                         INTERACT_ACROSS_USERS_FULL)) {
91             UserReference systemUser = TestApis.users().system();
92             File apk = new File(mContext.getCacheDir(), "InteractiveAutomation.apk");
93 
94             if (apk.exists()) {
95                 // We want to avoid caching the automations - TODO: This should probably be
96                 //  controlled by a flag and when the flag is off we don't attempt to copy over
97                 // again at all
98                 apk.delete();
99             }
100 
101             if (TestApis.users().instrumented().equals(systemUser)) {
102                 // Just need to copy to internal
103 
104                 if (!new File(mAutomationFile).exists()) {
105                     return new File(mAutomationFile); // Doesn't exist
106                 }
107 
108                 try (FileInputStream is = new FileInputStream(mAutomationFile);
109                      FileOutputStream os = new FileOutputStream(apk)) {
110                     apk.setReadOnly();
111                     copy(is, os);
112                 } catch (IOException e) {
113                     // Handle error
114                     Log.e("Interactive.Automator", "Error reading automation file", e);
115                 }
116 
117                 return apk;
118             }
119 
120             Package pkg = TestApis.packages().instrumented();
121             // Otherwise, let's try to fetch the file from the system user
122             boolean mustBeUninstalled = false;
123             try {
124                 mustBeUninstalled = !pkg.installedOnUser(systemUser);
125 
126                 if (mustBeUninstalled) {
127                     pkg.installExisting(systemUser);
128                 }
129 
130                 pkg.appOps(systemUser).set(OPSTR_MANAGE_EXTERNAL_STORAGE, AppOpsMode.ALLOWED);
131                 try (ParcelFileDescriptor remoteFile = mContext.createContextAsUser(
132                         systemUser.userHandle(), /* flags= */0)
133                         .getContentResolver()
134                         .openFile(Uri.parse("content://"
135                                         + mContext.getPackageName()
136                                         + ".interactive.automation"), "r",
137                         new CancellationSignal());
138                      InputStream fileStream = new FileInputStream(remoteFile.getFileDescriptor());
139                      OutputStream outputStream = new FileOutputStream(apk)) {
140 
141                     apk.setReadOnly();
142                     copy(fileStream, outputStream);
143                 } catch (IOException e) {
144                     e.printStackTrace();
145                 }
146 
147                 return apk;
148             } finally {
149                 if (mustBeUninstalled) {
150                     pkg.uninstall(systemUser);
151                 }
152             }
153         }
154     }
155 
init()156     private void init() {
157         if (mHasInitialised) {
158             return;
159         }
160 
161         mHasInitialised = true;
162 
163         try (PermissionContext p = TestApis.permissions()
164                 .withPermission(READ_EXTERNAL_STORAGE, MANAGE_EXTERNAL_STORAGE)) {
165 
166             File automation = copyAutomationFileToInternalStorage();
167 
168             if (!automation.exists()) {
169                 Log.e(LOG_TAG, "Automation file does not exist");
170                 return;
171             }
172 
173             final File optimizedDexOutputPath = mContext.getDir("outdex", 0);
174             DexClassLoader dLoader =
175                     new DexClassLoader(automation.getAbsolutePath(), optimizedDexOutputPath.getAbsolutePath(),
176                     null, ClassLoader.getSystemClassLoader());
177 
178             try {
179                 Class<?> instrumentationRegistryClass =
180                         dLoader.loadClass("androidx.test.platform.app.InstrumentationRegistry");
181                 instrumentationRegistryClass
182                         .getMethod("registerInstance", android.app.Instrumentation.class,
183                                 android.os.Bundle.class).invoke(null,
184                                 InstrumentationRegistry.getInstrumentation(),
185                                 InstrumentationRegistry.getArguments());
186             } catch (InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) {
187                 throw new RuntimeException("Error sharing instrumentation with automation", e);
188             }
189 
190             DexFile dx = DexFile
191                     .loadDex(automation.getAbsolutePath(), File.createTempFile("opt", "dex",
192                             mContext.getCacheDir()).getPath(), 0);
193             for (Enumeration<String> classNames = dx.entries(); classNames.hasMoreElements();) {
194                 String className = classNames.nextElement();
195                 try {
196                     Class<?> cls = dLoader.loadClass(className);
197                     String automationFor = getAutomationFor(cls);
198                     if (automationFor != null) {
199                         // TODO: We need to check that the data type of the automation matches the
200                         //  data type of the step
201                         mAutomationClasses.put(automationFor,
202                                 new AutomationExecutor(cls.newInstance()));
203                     }
204                 } catch (ClassNotFoundException e) {
205                     // If we can't load the class we just assume it's not an automation
206                     Log.i(LOG_TAG, "Error loading potential automation class", e);
207                 }
208             }
209         } catch (IOException | InstantiationException | IllegalAccessException e) {
210             Log.e(LOG_TAG, "Error loading automations", e);
211         }
212     }
213 
getAutomationFor(Class<?> cls)214     private String getAutomationFor(Class<?> cls) {
215         for (Annotation annotation : cls.getAnnotations()) {
216             if (annotation.annotationType().getCanonicalName().equals(
217                     AutomationFor.class.getCanonicalName())) {
218                 try {
219                     return (String) annotation.annotationType().getMethod("value")
220                             .invoke(annotation);
221                 } catch (IllegalAccessException | InvocationTargetException
222                         | NoSuchMethodException e) {
223                     throw new RuntimeException(
224                             "Error getting automation details for " + annotation);
225                 }
226             }
227         }
228         return null;
229     }
230 
231     /**
232      * Returns true if there is a valid automation for the given step.
233      */
canAutomate(Step step)234     public boolean canAutomate(Step step) {
235         init();
236 
237         return (mAutomationClasses.containsKey(step.getClass().getCanonicalName()));
238     }
239 
240     /**
241      * Run automation for a given step.
242      *
243      * <p>{@link #canAutomate(Step)} should be returning true before calling this.
244      */
automate(Step<E> step)245     public <E> E automate(Step<E> step) throws Exception {
246         // Unchecked cast is okay as we've verified the types when inserting into the map
247         return ((Automation<E>)mAutomationClasses.get(step.getClass().getCanonicalName()))
248                 .automate();
249     }
250 }
251