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.support.test.uiautomator.UiDevice; 25 import android.util.Log; 26 27 import dalvik.system.DexFile; 28 29 import java.io.File; 30 import java.io.IOException; 31 import java.lang.ClassLoader; 32 import java.lang.ClassNotFoundException; 33 import java.lang.IllegalAccessException; 34 import java.lang.InstantiationException; 35 import java.lang.NoSuchMethodException; 36 import java.lang.reflect.Constructor; 37 import java.lang.reflect.InvocationTargetException; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.Collections; 41 import java.util.Enumeration; 42 import java.util.List; 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 private static final String LOG_TAG = HelperManager.class.getSimpleName(); 64 private static HelperManager sInstance; 65 66 /** 67 * Returns an instance of the HelperManager that searches the supplied application context for 68 * classes to instantiate to helper implementations. 69 * 70 * @param context the application context to search 71 * @param instr the active instrumentation 72 * @return a new instance of the HelperManager class 73 */ getInstance(Context context, Instrumentation instr)74 public static HelperManager getInstance(Context context, Instrumentation instr) { 75 if (sInstance == null) { 76 // Input checks 77 if (context == null) { 78 throw new NullPointerException("Cannot pass in a null context."); 79 } 80 if (instr == null) { 81 throw new NullPointerException( 82 String.format("Cannot pass in null instrumentation.")); 83 } 84 // Instantiation 85 List<String> paths = Arrays.asList(context.getPackageCodePath()); 86 sInstance = new HelperManager(paths, instr); 87 } 88 return sInstance; 89 } 90 91 /** 92 * Returns an instance of the HelperManager that searches the supplied locations for classes to 93 * instantiate to helper implementations. 94 * 95 * @param paths the dex files where the classes are included 96 * @param instr the active instrumentation 97 * @throws IllegalArgumentException if the path is not a valid file 98 * @return a new instance of the HelperManager class 99 */ getInstance(List<String> paths, Instrumentation instr)100 public static HelperManager getInstance(List<String> paths, Instrumentation instr) { 101 if (sInstance == null) { 102 // Input checks 103 for (String path : paths) { 104 if (!(new File(path)).exists()) { 105 throw new IllegalArgumentException( 106 String.format("No file found at path: %s.", path)); 107 } 108 } 109 if (instr == null) { 110 throw new NullPointerException( 111 String.format("Cannot pass in null instrumentation.")); 112 } 113 // Instantiation 114 sInstance = new HelperManager(paths, instr); 115 } 116 return sInstance; 117 } 118 119 private Instrumentation mInstrumentation; 120 private List<String> mClasses; 121 HelperManager(List<String> paths, Instrumentation instr)122 private HelperManager(List<String> paths, Instrumentation instr) { 123 mInstrumentation = instr; 124 // Collect all of the available classes 125 mClasses = new ArrayList<String>(); 126 try { 127 for (String path : paths) { 128 DexFile dex = new DexFile(path); 129 mClasses.addAll(Collections.list(dex.entries())); 130 } 131 } catch (IOException e) { 132 throw new RuntimeException("Failed to retrieve the dex file."); 133 } 134 } 135 136 /* 137 * Returns a concrete helper for the {@link ILauncherHelper}. 138 * 139 * @throws RuntimeException if no implementation is found 140 * @return a concrete implementation of {@link ILauncherHelper} 141 */ getLauncherHelper()142 public ILauncherHelper getLauncherHelper() { 143 UiDevice device = UiDevice.getInstance(mInstrumentation); 144 for (ILauncherHelper implementation : getAll(ILauncherHelper.class)) { 145 if (implementation.getLauncherName().equals(device.getLauncherPackageName())) { 146 return implementation; 147 } 148 } 149 150 throw new RuntimeException("Unable to find a supported launcher implementation."); 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 * @throws RuntimeException if no implementation is found 158 * @return a concrete implementation of base 159 */ get(Class<T> base)160 public <T extends IAppHelper> T get(Class<T> base) { 161 return get(base, ""); 162 } 163 164 /** 165 * Returns a concrete implementation of the helper interface supplied, if available. 166 * 167 * @param base the interface base class to find an implementation for 168 * @param prefix a prefix for matching the helper implementation, if multiple exist 169 * @throws RuntimeException if no implementation is found 170 * @return a concrete implementation of base 171 */ get(Class<T> base, String prefix)172 public <T extends IAppHelper> T get(Class<T> base, String prefix) { 173 List<T> implementations = getAll(base); 174 List<T> matching = new ArrayList<>(); 175 for (T implementation : implementations) { 176 if (implementation.getClass().getSimpleName().startsWith(prefix)) { 177 Log.i(LOG_TAG, "Found matching implementation: " 178 + implementation.getClass().getSimpleName()); 179 matching.add(implementation); 180 } 181 } 182 183 if (!matching.isEmpty()) { 184 T result = matching.get(0); 185 Log.i(LOG_TAG, "Selecting implementation: " + result.getClass().getSimpleName()); 186 return result; 187 } 188 189 throw new RuntimeException( 190 String.format("Failed to find an implementation for %s", base.toString())); 191 } 192 193 /** 194 * Returns all concrete implementations of the helper interface supplied. 195 * 196 * @param base the interface base class to find an implementation for 197 * @return a list of all concrete implementations we could find 198 */ getAll(Class<T> base)199 public <T extends IAppHelper> List<T> getAll(Class<T> base) { 200 ClassLoader loader = HelperManager.class.getClassLoader(); 201 List<T> implementations = new ArrayList<>(); 202 203 // Iterate and search for the implementation 204 for (String className : mClasses) { 205 Class<?> clazz = null; 206 try { 207 clazz = loader.loadClass(className); 208 // Skip non-instantiable classes 209 if (isAbstract(clazz.getModifiers()) || isInterface(clazz.getModifiers())) { 210 continue; 211 } 212 } catch (ClassNotFoundException e) { 213 Log.w(LOG_TAG, String.format("Class not found: %s", className)); 214 continue; 215 } 216 if (base.isAssignableFrom(clazz) && !clazz.equals(base)) { 217 218 // Instantiate the implementation class and return 219 try { 220 Constructor<?> constructor = clazz.getConstructor(Instrumentation.class); 221 implementations.add((T)constructor.newInstance(mInstrumentation)); 222 } catch (NoSuchMethodException e) { 223 Log.w(LOG_TAG, String.format("Failed to find a matching constructor for %s", 224 className), e); 225 } catch (IllegalAccessException e) { 226 Log.w(LOG_TAG, String.format("Failed to access the constructor %s", 227 className), e); 228 } catch (InstantiationException e) { 229 Log.w(LOG_TAG, String.format("Failed to instantiate %s", 230 className), e); 231 } catch (InvocationTargetException e) { 232 Log.w(LOG_TAG, String.format("Exception encountered instantiating %s", 233 className), e); 234 } 235 } 236 } 237 238 return implementations; 239 } 240 } 241