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.test.helpers; 18 19 import android.app.Instrumentation; 20 import android.content.Context; 21 import android.util.Log; 22 import dalvik.system.DexFile; 23 import java.io.File; 24 import java.io.IOException; 25 import java.lang.ClassLoader; 26 import java.lang.ClassNotFoundException; 27 import java.lang.IllegalAccessException; 28 import java.lang.InstantiationException; 29 import java.lang.NoSuchMethodException; 30 import java.lang.reflect.Constructor; 31 import java.lang.reflect.InvocationTargetException; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.Collections; 35 import java.util.Enumeration; 36 import java.util.List; 37 38 /** 39 * The HelperManager class is used to remove any explicit or hard-coded dependencies on app helper 40 * implementations. Instead, it provides a method for abstracting helper instantiation by specifying 41 * a base class for which you require an implementation. The manager effectively searches for a 42 * suitable implementation using runtime class loading. 43 * <p> 44 * The class provides two means for finding the necessary classes: 45 * <ol> 46 * <li> Static inclusion - if all the code is included in the final APK, the Context can be used to 47 * generate a HelperManager and to instantiate implementations. 48 * <li> Dexed file inclusion - if this manager and the helper implementations are bundled into dex 49 * files and loaded from a single class loader, then the files can be used to generate a 50 * HelperManager and to instantiate implementations. 51 * </ol> 52 * <p> 53 * Including and using this strategy will prune the explicit dependency tree for the App Helper 54 * Library and provide a more robust library for use across the Android source tree. 55 */ 56 public class HelperManager { 57 private static final String LOG_TAG = HelperManager.class.getSimpleName(); 58 private static HelperManager sInstance; 59 60 /** 61 * Returns an instance of the HelperManager that searches the supplied application context for 62 * classes to instantiate to helper implementations. 63 * 64 * @param context the application context to search 65 * @param instr the active instrumentation 66 * @return a new instance of the HelperManager class 67 */ getInstance(Context context, Instrumentation instr)68 public static HelperManager getInstance(Context context, Instrumentation instr) { 69 if (sInstance == null) { 70 // Input checks 71 if (context == null) { 72 throw new NullPointerException("Cannot pass in a null context."); 73 } 74 if (instr == null) { 75 throw new NullPointerException( 76 String.format("Cannot pass in null instrumentation.")); 77 } 78 // Instantiation 79 List<String> paths = Arrays.asList(context.getPackageCodePath()); 80 sInstance = new HelperManager(paths, instr); 81 } 82 return sInstance; 83 } 84 85 /** 86 * Returns an instance of the HelperManager that searches the supplied locations for classes to 87 * instantiate to helper implementations. 88 * 89 * @param paths the dex files where the classes are included 90 * @param instr the active instrumentation 91 * @throws IllegalArgumentException if the path is not a valid file 92 * @return a new instance of the HelperManager class 93 */ getInstance(List<String> paths, Instrumentation instr)94 public static HelperManager getInstance(List<String> paths, Instrumentation instr) { 95 if (sInstance == null) { 96 // Input checks 97 for (String path : paths) { 98 if (!(new File(path)).exists()) { 99 throw new IllegalArgumentException( 100 String.format("No file found at path: %s.", path)); 101 } 102 } 103 if (instr == null) { 104 throw new NullPointerException( 105 String.format("Cannot pass in null instrumentation.")); 106 } 107 // Instantiation 108 sInstance = new HelperManager(paths, instr); 109 } 110 return sInstance; 111 } 112 113 private Instrumentation mInstrumentation; 114 private List<String> mClasses; 115 HelperManager(List<String> paths, Instrumentation instr)116 private HelperManager(List<String> paths, Instrumentation instr) { 117 mInstrumentation = instr; 118 // Collect all of the available classes 119 mClasses = new ArrayList<String>(); 120 try { 121 for (String path : paths) { 122 DexFile dex = new DexFile(path); 123 mClasses.addAll(Collections.list(dex.entries())); 124 } 125 } catch (IOException e) { 126 throw new RuntimeException("Failed to retrieve the dex file."); 127 } 128 } 129 130 /** 131 * Returns a concrete implementation of the helper interface supplied, if available. 132 * 133 * @param base the interface base class to find an implementation for 134 * @throws RuntimeException if no implementation is found 135 * @return a concrete implementation of base 136 */ get(Class<T> base)137 public <T extends AbstractStandardAppHelper> T get(Class<T> base) { 138 ClassLoader loader = HelperManager.class.getClassLoader(); 139 // Iterate and search for the implementation 140 for (String className : mClasses) { 141 Class<?> clazz = null; 142 try { 143 clazz = loader.loadClass(className); 144 } catch (ClassNotFoundException e) { 145 Log.w(LOG_TAG, String.format("Class not found: %s", className)); 146 } 147 if (base.isAssignableFrom(clazz) && !clazz.equals(base)) { 148 // Instantiate the implementation class and return 149 try { 150 Constructor<?> constructor = clazz.getConstructor(Instrumentation.class); 151 return (T)constructor.newInstance(mInstrumentation); 152 } catch (NoSuchMethodException e) { 153 Log.w(LOG_TAG, String.format("Failed to find a matching constructor for %s", 154 className), e); 155 } catch (IllegalAccessException e) { 156 Log.w(LOG_TAG, String.format("Failed to access the constructor %s", 157 className), e); 158 } catch (InstantiationException e) { 159 Log.w(LOG_TAG, String.format("Failed to instantiate %s", 160 className), e); 161 } catch (InvocationTargetException e) { 162 Log.w(LOG_TAG, String.format("Exception encountered instantiating %s", 163 className), e); 164 } 165 } 166 } 167 throw new RuntimeException( 168 String.format("Failed to find an implementation for %s", base.toString())); 169 } 170 } 171