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