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