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