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