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