• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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