• 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.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