1 /* 2 * Copyright (C) 2016 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 android.platform.helpers; 18 19 import static java.lang.reflect.Modifier.isAbstract; 20 import static java.lang.reflect.Modifier.isInterface; 21 22 import android.app.Instrumentation; 23 import android.content.Context; 24 import android.platform.helpers.exceptions.MappedMultiException; 25 import android.platform.helpers.exceptions.TestHelperException; 26 import android.util.Log; 27 28 import dalvik.system.DexFile; 29 import dalvik.system.PathClassLoader; 30 31 import java.io.File; 32 import java.io.IOException; 33 import java.lang.reflect.Constructor; 34 import java.lang.reflect.InvocationTargetException; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.Collections; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.regex.Pattern; 42 import java.util.stream.Collectors; 43 44 /** 45 * The HelperManager class is used to remove any explicit or hard-coded dependencies on app helper 46 * implementations. Instead, it provides a method for abstracting helper instantiation by specifying 47 * a base class for which you require an implementation. The manager effectively searches for a 48 * suitable implementation using runtime class loading. 49 * <p> 50 * The class provides two means for finding the necessary classes: 51 * <ol> 52 * <li> Static inclusion - if all the code is included in the final APK, the Context can be used to 53 * generate a HelperManager and to instantiate implementations. 54 * <li> Dexed file inclusion - if this manager and the helper implementations are bundled into dex 55 * files and loaded from a single class loader, then the files can be used to generate a 56 * HelperManager and to instantiate implementations. Use of this is discouraged. 57 * </ol> 58 * <p> 59 * Including and using this strategy will prune the explicit dependency tree for the App Helper 60 * Library and provide a more robust library for use across the Android source tree. 61 */ 62 public class HelperManager { 63 public static final String NO_MATCH_ERROR_MESSAGE = "No matching implementations"; 64 65 private static final String LOG_TAG = HelperManager.class.getSimpleName(); 66 private static HelperManager sInstance; 67 68 /** 69 * Returns an instance of the HelperManager that searches the supplied application context for 70 * classes to instantiate to helper implementations. 71 * 72 * @param context the application context to search 73 * @param instr the active instrumentation 74 * @return a new instance of the HelperManager class 75 */ getInstance(Context context, Instrumentation instr)76 public static HelperManager getInstance(Context context, Instrumentation instr) { 77 if (sInstance == null) { 78 // Input checks 79 if (context == null) { 80 throw new NullPointerException("Cannot pass in a null context."); 81 } 82 if (instr == null) { 83 throw new NullPointerException( 84 String.format("Cannot pass in null instrumentation.")); 85 } 86 // Instantiation 87 List<String> paths = Arrays.asList(context.getPackageCodePath()); 88 sInstance = new HelperManager(paths, instr); 89 } 90 return sInstance; 91 } 92 93 /** 94 * Returns an instance of the HelperManager that searches the supplied locations for classes to 95 * instantiate to helper implementations. 96 * 97 * @param paths the dex files where the classes are included 98 * @param instr the active instrumentation 99 * @throws IllegalArgumentException if the path is not a valid file 100 * @return a new instance of the HelperManager class 101 */ getInstance(List<String> paths, Instrumentation instr)102 public static HelperManager getInstance(List<String> paths, Instrumentation instr) { 103 if (sInstance == null) { 104 // Input checks 105 for (String path : paths) { 106 if (!(new File(path)).exists()) { 107 throw new IllegalArgumentException( 108 String.format("No file found at path: %s.", path)); 109 } 110 } 111 if (instr == null) { 112 throw new NullPointerException( 113 String.format("Cannot pass in null instrumentation.")); 114 } 115 // Instantiation 116 sInstance = new HelperManager(paths, instr); 117 } 118 return sInstance; 119 } 120 121 private Instrumentation mInstrumentation; 122 private List<String> mClasses; 123 private ClassLoader mLoader; 124 HelperManager(List<String> paths, Instrumentation instr)125 private HelperManager(List<String> paths, Instrumentation instr) { 126 mInstrumentation = instr; 127 // Collect all of the available classes 128 mClasses = new ArrayList<String>(); 129 try { 130 for (String path : paths) { 131 DexFile dex = new DexFile(path); 132 mClasses.addAll(Collections.list(dex.entries())); 133 } 134 mLoader = 135 new PathClassLoader( 136 String.join(":", paths), HelperManager.class.getClassLoader()); 137 } catch (IOException e) { 138 throw new TestHelperException("Failed to retrieve the dex file."); 139 } 140 } 141 142 /** 143 * Returns a concrete implementation of the helper interface supplied, if available. 144 * 145 * @param base the interface base class to find an implementation for 146 * @throws TestHelperException if no implementation is found 147 * @return a concrete implementation of base 148 */ get(Class<T> base)149 public <T extends ITestHelper> T get(Class<T> base) { 150 return get(base, ""); 151 } 152 153 /** 154 * Returns a concrete implementation of the helper interface supplied, if available. 155 * 156 * @param base the interface base class to find an implementation for 157 * @param keyword a keyword for matching the helper implementation, if multiple exist 158 * @throws TestHelperException if no implementation is found 159 * @return a list of all concrete implementations we could find 160 */ get(Class<T> base, String keyword)161 public <T extends ITestHelper> T get(Class<T> base, String keyword) { 162 List<T> matching = getAll(base, keyword); 163 Log.i( 164 LOG_TAG, 165 String.format("Selecting implementation %s", matching.get(0).getClass().getName())); 166 return matching.get(0); 167 } 168 169 /** 170 * Returns a concrete implementation of the helper interface supplied, if available. 171 * 172 * @param base the interface base class to find an implementation for 173 * @param regex a regular expression for matching the helper implementation, if multiple exist 174 * @throws TestHelperException if no implementation is found 175 * @return a list of all concrete implementations we could find 176 */ get(Class<T> base, Pattern regex)177 public <T extends ITestHelper> T get(Class<T> base, Pattern regex) { 178 List<T> matching = getAll(base, regex); 179 Log.i( 180 LOG_TAG, 181 String.format("Selecting implementation %s", matching.get(0).getClass().getName())); 182 return matching.get(0); 183 } 184 185 /** 186 * Returns a concrete implementation of the helper interface supplied, if available. 187 * 188 * @param base the interface base class to find an implementation for 189 * @param keyword a keyword for matching the helper implementation, if multiple exist 190 * @throws TestHelperException if no implementation is found 191 * @return a concrete implementation of base 192 */ getAll(Class<T> base, String keyword)193 private <T extends ITestHelper> List<T> getAll(Class<T> base, String keyword) { 194 Pattern p = Pattern.compile(".*\\Q" + keyword + "\\E.*"); 195 return getAll(base, p); 196 } 197 198 /** 199 * Returns a concrete implementation of the helper interface supplied, if available. 200 * 201 * @param base the interface base class to find an implementation for 202 * @param regex a regular expression for matching the helper implementation, if multiple exist 203 * @throws TestHelperException if no implementation is found 204 * @return a concrete implementation of base 205 */ getAll(Class<T> base, Pattern regex)206 private <T extends ITestHelper> List<T> getAll(Class<T> base, Pattern regex) { 207 List<T> implementations = new ArrayList<>(); 208 Map<Object, Throwable> mappedExceptions = new HashMap<>(); 209 210 // Iterate and search for the implementation 211 for (String className : mClasses) { 212 Class<?> clazz = null; 213 try { 214 clazz = mLoader.loadClass(className); 215 // Skip non-instantiable classes 216 if (isAbstract(clazz.getModifiers()) || isInterface(clazz.getModifiers())) { 217 continue; 218 } 219 } catch (ClassNotFoundException e) { 220 Log.w(LOG_TAG, String.format("Class not found: %s", className)); 221 continue; 222 } 223 if (base.isAssignableFrom(clazz) 224 && !clazz.equals(base) 225 && regex.matcher(className).matches()) { 226 // Instantiate the implementation class and return 227 try { 228 Constructor<?> constructor = clazz.getConstructor(Instrumentation.class); 229 implementations.add((T)constructor.newInstance(mInstrumentation)); 230 } catch (NoSuchMethodException e) { 231 mappedExceptions.put( 232 clazz, 233 wrapThrowable( 234 String.format( 235 "Failed to find a matching constructor for %s", 236 className), 237 e)); 238 } catch (IllegalAccessException e) { 239 mappedExceptions.put( 240 clazz, 241 wrapThrowable( 242 String.format("Failed to access the constructor %s", className), 243 e)); 244 } catch (InstantiationException e) { 245 mappedExceptions.put( 246 clazz, 247 wrapThrowable(String.format("Failed to instantiate %s", className), e)); 248 } catch (InvocationTargetException e) { 249 mappedExceptions.put( 250 clazz, 251 wrapThrowable( 252 String.format( 253 "Exception encountered instantiating %s", className), 254 e)); 255 } 256 } 257 } 258 259 if (implementations.isEmpty()) { 260 if (mappedExceptions.isEmpty()) { 261 throw new TestHelperException( 262 String.format( 263 "Could not find an implementation for %s. %s.", 264 base, NO_MATCH_ERROR_MESSAGE)); 265 } 266 throw new MappedMultiException( 267 String.format( 268 "Could not find an implementation for %s. " 269 + "Instantiation for all candidates failed. " 270 + "Please look at the error messages below to determine why.", 271 base), 272 mappedExceptions); 273 } 274 275 Log.d( 276 LOG_TAG, 277 String.format( 278 "Found matching implementations: %s.", 279 implementations 280 .stream() 281 .map(i -> i.getClass().getName()) 282 .collect(Collectors.toList()))); 283 284 return implementations; 285 } 286 287 /** Wrap the {@link Throwable} in a {@link TestHelperException} with a custom error message. */ wrapThrowable(String message, Throwable t)288 private TestHelperException wrapThrowable(String message, Throwable t) { 289 Throwable causeIfPresent = getCauseIfPresent(t); 290 // According to the documentation of RuntimeException the message for the causing Throwable 291 // needs to be included explicitly. 292 TestHelperException re = 293 new TestHelperException( 294 String.format("%s:\n%s.", message, causeIfPresent.getMessage()), 295 causeIfPresent); 296 return re; 297 } 298 299 /** Get the cause of a {@link Throwable}, or itself if one is not present. */ getCauseIfPresent(Throwable t)300 private Throwable getCauseIfPresent(Throwable t) { 301 return t.getCause() == null ? t : t.getCause(); 302 } 303 } 304