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