OLD | NEW |
| (Empty) |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package org.chromium.webapk.shell_apk; | |
6 | |
7 import android.content.ComponentName; | |
8 import android.content.Context; | |
9 import android.content.Intent; | |
10 import android.content.ServiceConnection; | |
11 import android.os.FileObserver; | |
12 import android.os.IBinder; | |
13 import android.os.RemoteException; | |
14 import android.test.InstrumentationTestCase; | |
15 import android.test.suitebuilder.annotation.MediumTest; | |
16 | |
17 import dalvik.system.DexFile; | |
18 | |
19 import org.chromium.base.FileUtils; | |
20 import org.chromium.content.browser.test.util.CallbackHelper; | |
21 import org.chromium.webapk.lib.common.WebApkUtils; | |
22 import org.chromium.webapk.shell_apk.test.dex_optimizer.IDexOptimizerService; | |
23 | |
24 import java.io.File; | |
25 import java.util.ArrayList; | |
26 import java.util.Arrays; | |
27 | |
28 public class DexLoaderTest extends InstrumentationTestCase { | |
29 /** | |
30 * Package of APK to load dex file from and package which provides DexOptimi
zerService. | |
31 */ | |
32 private static final String DEX_OPTIMIZER_SERVICE_PACKAGE = | |
33 "org.chromium.webapk.shell_apk.test.dex_optimizer"; | |
34 | |
35 /** | |
36 * Class which implements DexOptimizerService. | |
37 */ | |
38 private static final String DEX_OPTIMIZER_SERVICE_CLASS_NAME = | |
39 "org.chromium.webapk.shell_apk.test.dex_optimizer.DexOptimizerServic
eImpl"; | |
40 | |
41 /** | |
42 * Name of the dex file in DexOptimizer.apk. | |
43 */ | |
44 private static final String DEX_ASSET_NAME = "canary.dex"; | |
45 | |
46 /** | |
47 * Class to load to check whether dex is valid. | |
48 */ | |
49 private static final String CANARY_CLASS_NAME = | |
50 "org.chromium.webapk.shell_apk.test.canary.Canary"; | |
51 | |
52 private Context mContext; | |
53 private Context mRemoteContext; | |
54 private File mRemoteDexFile; | |
55 private File mLocalDexDir; | |
56 private IDexOptimizerService mDexOptimizerService; | |
57 private ServiceConnection mServiceConnection; | |
58 | |
59 /** | |
60 * Monitors read files and modified files in the directory passed to the con
structor. | |
61 */ | |
62 private class FileMonitor extends FileObserver { | |
63 public ArrayList<String> mReadPaths = new ArrayList<String>(); | |
64 public ArrayList<String> mModifiedPaths = new ArrayList<String>(); | |
65 | |
66 public FileMonitor(File directory) { | |
67 super(directory.getPath()); | |
68 } | |
69 | |
70 @Override | |
71 public void onEvent(int event, String path) { | |
72 switch (event) { | |
73 case FileObserver.ACCESS: | |
74 mReadPaths.add(path); | |
75 break; | |
76 case FileObserver.CREATE: | |
77 case FileObserver.DELETE: | |
78 case FileObserver.DELETE_SELF: | |
79 case FileObserver.MODIFY: | |
80 mModifiedPaths.add(path); | |
81 break; | |
82 default: | |
83 break; | |
84 } | |
85 } | |
86 } | |
87 | |
88 @Override | |
89 protected void setUp() { | |
90 mContext = getInstrumentation().getTargetContext(); | |
91 | |
92 mRemoteContext = WebApkUtils.getHostBrowserContext(mContext); | |
93 if (mRemoteContext == null) { | |
94 fail("Failed to get remote context."); | |
95 } | |
96 | |
97 mLocalDexDir = mContext.getDir("dex", Context.MODE_PRIVATE); | |
98 if (mLocalDexDir.exists()) { | |
99 FileUtils.recursivelyDeleteFile(mLocalDexDir); | |
100 if (mLocalDexDir.exists()) { | |
101 fail("Could not delete local dex directory."); | |
102 } | |
103 } | |
104 | |
105 connectToDexOptimizerService(); | |
106 | |
107 try { | |
108 if (!mDexOptimizerService.deleteDexDirectory()) { | |
109 fail("Could not delete remote dex directory."); | |
110 } | |
111 } catch (RemoteException e) { | |
112 e.printStackTrace(); | |
113 fail("Remote crashed during setup."); | |
114 } | |
115 } | |
116 | |
117 @Override | |
118 public void tearDown() { | |
119 mContext.unbindService(mServiceConnection); | |
120 } | |
121 | |
122 /** | |
123 * Test that {@DexLoader#load()} can create a ClassLoader from a dex and opt
imized dex in | |
124 * another app's data directory. | |
125 */ | |
126 @MediumTest | |
127 public void testLoadFromRemoteDataDir() { | |
128 // Extract the dex file into another app's data directory and optimize t
he dex. | |
129 String remoteDexFilePath = null; | |
130 try { | |
131 remoteDexFilePath = mDexOptimizerService.extractAndOptimizeDex(); | |
132 } catch (RemoteException e) { | |
133 e.printStackTrace(); | |
134 fail("Remote crashed."); | |
135 } | |
136 | |
137 if (remoteDexFilePath == null) { | |
138 fail("Could not extract and optimize dex."); | |
139 } | |
140 | |
141 // Check that the Android OS knows about the optimized dex file for | |
142 // {@link remoteDexFilePath}. | |
143 File remoteDexFile = new File(remoteDexFilePath); | |
144 assertFalse(isDexOptNeeded(remoteDexFile)); | |
145 | |
146 ClassLoader loader = DexLoader.load( | |
147 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, remoteDexFile
, mLocalDexDir); | |
148 assertNotNull(loader); | |
149 assertTrue(canLoadCanaryClass(loader)); | |
150 | |
151 // Check that {@link DexLoader#load()} did not use the fallback path. | |
152 assertFalse(mLocalDexDir.exists()); | |
153 } | |
154 | |
155 /** | |
156 * That that {@link DexLoader#load()} falls back to extracting the dex from
the APK to the | |
157 * local data directory and creating the ClassLoader from the extracted dex
if creating the | |
158 * ClassLoader from the cached data in the remote Context's data directory f
ails. | |
159 */ | |
160 @MediumTest | |
161 public void testLoadFromLocalDataDir() { | |
162 ClassLoader loader = DexLoader.load( | |
163 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalD
exDir); | |
164 assertNotNull(loader); | |
165 assertTrue(canLoadCanaryClass(loader)); | |
166 | |
167 // Check that the dex file was extracted to the local data directory and
that a directory | |
168 // was created for the optimized dex. | |
169 assertTrue(mLocalDexDir.exists()); | |
170 File[] localDexDirFiles = mLocalDexDir.listFiles(); | |
171 Arrays.sort(mLocalDexDir.listFiles()); | |
172 assertEquals(2, localDexDirFiles.length); | |
173 assertEquals(DEX_ASSET_NAME, localDexDirFiles[0].getName()); | |
174 assertFalse(localDexDirFiles[0].isDirectory()); | |
175 assertEquals("optimized", localDexDirFiles[1].getName()); | |
176 assertTrue(localDexDirFiles[1].isDirectory()); | |
177 } | |
178 | |
179 /** | |
180 * Test that {@link DexLoader#load()} does not extract the dex file from the
APK if the dex file | |
181 * was extracted in a previous call to {@link DexLoader#load()} | |
182 */ | |
183 @MediumTest | |
184 public void testPreviouslyLoadedFromLocalDataDir() { | |
185 mLocalDexDir.mkdir(); | |
186 | |
187 { | |
188 // Load dex the first time. This should extract the dex file from th
e APK's assets and | |
189 // generate the optimized dex file. | |
190 FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir); | |
191 localDexDirMonitor.startWatching(); | |
192 ClassLoader loader = DexLoader.load( | |
193 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLo
calDexDir); | |
194 localDexDirMonitor.stopWatching(); | |
195 | |
196 assertNotNull(loader); | |
197 assertTrue(canLoadCanaryClass(loader)); | |
198 | |
199 assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME)); | |
200 assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME
)); | |
201 } | |
202 { | |
203 // Load dex a second time. We should use the already extracted dex f
ile. | |
204 FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir); | |
205 localDexDirMonitor.startWatching(); | |
206 ClassLoader loader = DexLoader.load( | |
207 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLo
calDexDir); | |
208 localDexDirMonitor.stopWatching(); | |
209 | |
210 // The returned ClassLoader should be valid. | |
211 assertNotNull(loader); | |
212 assertTrue(canLoadCanaryClass(loader)); | |
213 | |
214 // We should not have modified any files and have used the already e
xtracted dex file. | |
215 assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME)); | |
216 assertTrue(localDexDirMonitor.mModifiedPaths.isEmpty()); | |
217 } | |
218 } | |
219 | |
220 /** | |
221 * Test that {@link DexLoader#load()} re-extracts the dex file from the APK
after a call to | |
222 * {@link DexLoader#deleteCachedDexes()}. | |
223 */ | |
224 @MediumTest | |
225 public void testLoadAfterDeleteCachedDexes() { | |
226 mLocalDexDir.mkdir(); | |
227 | |
228 { | |
229 // Load dex the first time. This should extract the dex file from th
e APK's assets and | |
230 // generate the optimized dex file. | |
231 FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir); | |
232 localDexDirMonitor.startWatching(); | |
233 ClassLoader loader = DexLoader.load( | |
234 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLo
calDexDir); | |
235 localDexDirMonitor.stopWatching(); | |
236 | |
237 assertNotNull(loader); | |
238 assertTrue(canLoadCanaryClass(loader)); | |
239 | |
240 assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME)); | |
241 assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME
)); | |
242 } | |
243 | |
244 DexLoader.deleteCachedDexes(mLocalDexDir); | |
245 | |
246 { | |
247 // Load dex a second time. | |
248 FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir); | |
249 localDexDirMonitor.startWatching(); | |
250 ClassLoader loader = DexLoader.load( | |
251 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLo
calDexDir); | |
252 localDexDirMonitor.stopWatching(); | |
253 | |
254 // The returned ClassLoader should be valid. | |
255 assertNotNull(loader); | |
256 assertTrue(canLoadCanaryClass(loader)); | |
257 | |
258 // We should have re-extracted the dex from the APK's assets. | |
259 assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME)); | |
260 assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME
)); | |
261 } | |
262 } | |
263 | |
264 /** | |
265 * Connects to the DexOptimizerService. | |
266 */ | |
267 private void connectToDexOptimizerService() { | |
268 Intent intent = new Intent(); | |
269 intent.setComponent( | |
270 new ComponentName(DEX_OPTIMIZER_SERVICE_PACKAGE, DEX_OPTIMIZER_S
ERVICE_CLASS_NAME)); | |
271 final CallbackHelper connectedCallback = new CallbackHelper(); | |
272 | |
273 mServiceConnection = new ServiceConnection() { | |
274 @Override | |
275 public void onServiceConnected(ComponentName name, IBinder service)
{ | |
276 mDexOptimizerService = IDexOptimizerService.Stub.asInterface(ser
vice); | |
277 connectedCallback.notifyCalled(); | |
278 } | |
279 | |
280 @Override | |
281 public void onServiceDisconnected(ComponentName name) {} | |
282 }; | |
283 | |
284 try { | |
285 mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_C
REATE); | |
286 } catch (SecurityException e) { | |
287 e.printStackTrace(); | |
288 fail(); | |
289 } | |
290 | |
291 try { | |
292 connectedCallback.waitForCallback(0); | |
293 } catch (Exception e) { | |
294 e.printStackTrace(); | |
295 fail("Could not connect to remote."); | |
296 } | |
297 } | |
298 | |
299 /** Returns whether the Android OS thinks that a dex file needs to be re-opt
imized */ | |
300 private boolean isDexOptNeeded(File dexFile) { | |
301 try { | |
302 return DexFile.isDexOptNeeded(dexFile.getPath()); | |
303 } catch (Exception e) { | |
304 e.printStackTrace(); | |
305 fail(); | |
306 return false; | |
307 } | |
308 } | |
309 | |
310 /** Returns whether the ClassLoader can load {@link CANARY_CLASS_NAME} */ | |
311 private boolean canLoadCanaryClass(ClassLoader loader) { | |
312 try { | |
313 loader.loadClass(CANARY_CLASS_NAME); | |
314 return true; | |
315 } catch (Exception e) { | |
316 return false; | |
317 } | |
318 } | |
319 } | |
OLD | NEW |