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.content.pm.PackageManager.NameNotFoundException; |
| 12 import android.os.FileObserver; |
| 13 import android.os.IBinder; |
| 14 import android.os.RemoteException; |
| 15 import android.test.InstrumentationTestCase; |
| 16 import android.test.suitebuilder.annotation.MediumTest; |
| 17 |
| 18 import dalvik.system.DexFile; |
| 19 |
| 20 import org.chromium.base.FileUtils; |
| 21 import org.chromium.content.browser.test.util.CallbackHelper; |
| 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 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 static 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 mRemoteContext = getRemoteContext(mContext); |
| 91 |
| 92 mLocalDexDir = mContext.getDir("dex", Context.MODE_PRIVATE); |
| 93 if (mLocalDexDir.exists()) { |
| 94 FileUtils.recursivelyDeleteFile(mLocalDexDir); |
| 95 if (mLocalDexDir.exists()) { |
| 96 fail("Could not delete local dex directory."); |
| 97 } |
| 98 } |
| 99 |
| 100 connectToDexOptimizerService(); |
| 101 |
| 102 try { |
| 103 if (!mDexOptimizerService.deleteDexDirectory()) { |
| 104 fail("Could not delete remote dex directory."); |
| 105 } |
| 106 } catch (RemoteException e) { |
| 107 e.printStackTrace(); |
| 108 fail("Remote crashed during setup."); |
| 109 } |
| 110 } |
| 111 |
| 112 @Override |
| 113 public void tearDown() throws Exception { |
| 114 mContext.unbindService(mServiceConnection); |
| 115 super.tearDown(); |
| 116 } |
| 117 |
| 118 /** |
| 119 * Test that {@DexLoader#load()} can create a ClassLoader from a dex and opt
imized dex in |
| 120 * another app's data directory. |
| 121 */ |
| 122 @MediumTest |
| 123 public void testLoadFromRemoteDataDir() { |
| 124 // Extract the dex file into another app's data directory and optimize t
he dex. |
| 125 String remoteDexFilePath = null; |
| 126 try { |
| 127 remoteDexFilePath = mDexOptimizerService.extractAndOptimizeDex(); |
| 128 } catch (RemoteException e) { |
| 129 e.printStackTrace(); |
| 130 fail("Remote crashed."); |
| 131 } |
| 132 |
| 133 if (remoteDexFilePath == null) { |
| 134 fail("Could not extract and optimize dex."); |
| 135 } |
| 136 |
| 137 // Check that the Android OS knows about the optimized dex file for |
| 138 // {@link remoteDexFilePath}. |
| 139 File remoteDexFile = new File(remoteDexFilePath); |
| 140 assertFalse(isDexOptNeeded(remoteDexFile)); |
| 141 |
| 142 ClassLoader loader = DexLoader.load( |
| 143 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, remoteDexFile
, mLocalDexDir); |
| 144 assertNotNull(loader); |
| 145 assertTrue(canLoadCanaryClass(loader)); |
| 146 |
| 147 // Check that {@link DexLoader#load()} did not use the fallback path. |
| 148 assertFalse(mLocalDexDir.exists()); |
| 149 } |
| 150 |
| 151 /** |
| 152 * That that {@link DexLoader#load()} falls back to extracting the dex from
the APK to the |
| 153 * local data directory and creating the ClassLoader from the extracted dex
if creating the |
| 154 * ClassLoader from the cached data in the remote Context's data directory f
ails. |
| 155 */ |
| 156 @MediumTest |
| 157 public void testLoadFromLocalDataDir() { |
| 158 ClassLoader loader = DexLoader.load( |
| 159 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalD
exDir); |
| 160 assertNotNull(loader); |
| 161 assertTrue(canLoadCanaryClass(loader)); |
| 162 |
| 163 // Check that the dex file was extracted to the local data directory and
that a directory |
| 164 // was created for the optimized dex. |
| 165 assertTrue(mLocalDexDir.exists()); |
| 166 File[] localDexDirFiles = mLocalDexDir.listFiles(); |
| 167 assertNotNull(localDexDirFiles); |
| 168 Arrays.sort(localDexDirFiles); |
| 169 assertEquals(2, localDexDirFiles.length); |
| 170 assertEquals(DEX_ASSET_NAME, localDexDirFiles[0].getName()); |
| 171 assertFalse(localDexDirFiles[0].isDirectory()); |
| 172 assertEquals("optimized", localDexDirFiles[1].getName()); |
| 173 assertTrue(localDexDirFiles[1].isDirectory()); |
| 174 } |
| 175 |
| 176 /** |
| 177 * Test that {@link DexLoader#load()} does not extract the dex file from the
APK if the dex file |
| 178 * was extracted in a previous call to {@link DexLoader#load()} |
| 179 */ |
| 180 @MediumTest |
| 181 public void testPreviouslyLoadedFromLocalDataDir() { |
| 182 assertTrue(mLocalDexDir.mkdir()); |
| 183 |
| 184 { |
| 185 // Load dex the first time. This should extract the dex file from th
e APK's assets and |
| 186 // generate the optimized dex file. |
| 187 FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir); |
| 188 localDexDirMonitor.startWatching(); |
| 189 ClassLoader loader = DexLoader.load( |
| 190 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLo
calDexDir); |
| 191 localDexDirMonitor.stopWatching(); |
| 192 |
| 193 assertNotNull(loader); |
| 194 assertTrue(canLoadCanaryClass(loader)); |
| 195 |
| 196 assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME)); |
| 197 assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME
)); |
| 198 } |
| 199 { |
| 200 // Load dex a second time. We should use the already extracted dex f
ile. |
| 201 FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir); |
| 202 localDexDirMonitor.startWatching(); |
| 203 ClassLoader loader = DexLoader.load( |
| 204 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLo
calDexDir); |
| 205 localDexDirMonitor.stopWatching(); |
| 206 |
| 207 // The returned ClassLoader should be valid. |
| 208 assertNotNull(loader); |
| 209 assertTrue(canLoadCanaryClass(loader)); |
| 210 |
| 211 // We should not have modified any files and have used the already e
xtracted dex file. |
| 212 assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME)); |
| 213 assertTrue(localDexDirMonitor.mModifiedPaths.isEmpty()); |
| 214 } |
| 215 } |
| 216 |
| 217 /** |
| 218 * Test that {@link DexLoader#load()} re-extracts the dex file from the APK
after a call to |
| 219 * {@link DexLoader#deleteCachedDexes()}. |
| 220 */ |
| 221 @MediumTest |
| 222 public void testLoadAfterDeleteCachedDexes() { |
| 223 assertTrue(mLocalDexDir.mkdir()); |
| 224 |
| 225 { |
| 226 // Load dex the first time. This should extract the dex file from th
e APK's assets and |
| 227 // generate the optimized dex file. |
| 228 FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir); |
| 229 localDexDirMonitor.startWatching(); |
| 230 ClassLoader loader = DexLoader.load( |
| 231 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLo
calDexDir); |
| 232 localDexDirMonitor.stopWatching(); |
| 233 |
| 234 assertNotNull(loader); |
| 235 assertTrue(canLoadCanaryClass(loader)); |
| 236 |
| 237 assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME)); |
| 238 assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME
)); |
| 239 } |
| 240 |
| 241 DexLoader.deleteCachedDexes(mLocalDexDir); |
| 242 |
| 243 { |
| 244 // Load dex a second time. |
| 245 FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir); |
| 246 localDexDirMonitor.startWatching(); |
| 247 ClassLoader loader = DexLoader.load( |
| 248 mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLo
calDexDir); |
| 249 localDexDirMonitor.stopWatching(); |
| 250 |
| 251 // The returned ClassLoader should be valid. |
| 252 assertNotNull(loader); |
| 253 assertTrue(canLoadCanaryClass(loader)); |
| 254 |
| 255 // We should have re-extracted the dex from the APK's assets. |
| 256 assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME)); |
| 257 assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME
)); |
| 258 } |
| 259 } |
| 260 |
| 261 /** |
| 262 * Connects to the DexOptimizerService. |
| 263 */ |
| 264 private void connectToDexOptimizerService() { |
| 265 Intent intent = new Intent(); |
| 266 intent.setComponent( |
| 267 new ComponentName(DEX_OPTIMIZER_SERVICE_PACKAGE, DEX_OPTIMIZER_S
ERVICE_CLASS_NAME)); |
| 268 final CallbackHelper connectedCallback = new CallbackHelper(); |
| 269 |
| 270 mServiceConnection = new ServiceConnection() { |
| 271 @Override |
| 272 public void onServiceConnected(ComponentName name, IBinder service)
{ |
| 273 mDexOptimizerService = IDexOptimizerService.Stub.asInterface(ser
vice); |
| 274 connectedCallback.notifyCalled(); |
| 275 } |
| 276 |
| 277 @Override |
| 278 public void onServiceDisconnected(ComponentName name) {} |
| 279 }; |
| 280 |
| 281 try { |
| 282 mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_C
REATE); |
| 283 } catch (SecurityException e) { |
| 284 e.printStackTrace(); |
| 285 fail(); |
| 286 } |
| 287 |
| 288 try { |
| 289 connectedCallback.waitForCallback(0); |
| 290 } catch (Exception e) { |
| 291 e.printStackTrace(); |
| 292 fail("Could not connect to remote."); |
| 293 } |
| 294 } |
| 295 |
| 296 /** |
| 297 * Returns the Context of the APK which provides DexOptimizerService. |
| 298 * @param context The test application's Context. |
| 299 * @return Context of the APK whcih provide DexOptimizerService. |
| 300 */ |
| 301 private Context getRemoteContext(Context context) { |
| 302 try { |
| 303 return context.getApplicationContext().createPackageContext( |
| 304 DEX_OPTIMIZER_SERVICE_PACKAGE, |
| 305 Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CO
DE); |
| 306 } catch (NameNotFoundException e) { |
| 307 e.printStackTrace(); |
| 308 fail("Could not get remote context"); |
| 309 return null; |
| 310 } |
| 311 } |
| 312 |
| 313 /** Returns whether the Android OS thinks that a dex file needs to be re-opt
imized */ |
| 314 private boolean isDexOptNeeded(File dexFile) { |
| 315 try { |
| 316 return DexFile.isDexOptNeeded(dexFile.getPath()); |
| 317 } catch (Exception e) { |
| 318 e.printStackTrace(); |
| 319 fail(); |
| 320 return false; |
| 321 } |
| 322 } |
| 323 |
| 324 /** Returns whether the ClassLoader can load {@link CANARY_CLASS_NAME} */ |
| 325 private boolean canLoadCanaryClass(ClassLoader loader) { |
| 326 try { |
| 327 loader.loadClass(CANARY_CLASS_NAME); |
| 328 return true; |
| 329 } catch (Exception e) { |
| 330 return false; |
| 331 } |
| 332 } |
| 333 } |
OLD | NEW |