blob: 442a1f0018a5c133bc1da6cb4bc6f4003ca0b225 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.webapk.shell_apk;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.FileObserver;
import android.os.IBinder;
import android.os.RemoteException;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import dalvik.system.DexFile;
import org.chromium.base.FileUtils;
import org.chromium.content.browser.test.util.CallbackHelper;
import org.chromium.webapk.shell_apk.test.dex_optimizer.IDexOptimizerService;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
public class DexLoaderTest extends InstrumentationTestCase {
/**
* Package of APK to load dex file from and package which provides DexOptimizerService.
*/
private static final String DEX_OPTIMIZER_SERVICE_PACKAGE =
"org.chromium.webapk.shell_apk.test.dex_optimizer";
/**
* Class which implements DexOptimizerService.
*/
private static final String DEX_OPTIMIZER_SERVICE_CLASS_NAME =
"org.chromium.webapk.shell_apk.test.dex_optimizer.DexOptimizerServiceImpl";
/**
* Name of the dex file in DexOptimizer.apk.
*/
private static final String DEX_ASSET_NAME = "canary.dex";
/**
* Class to load to check whether dex is valid.
*/
private static final String CANARY_CLASS_NAME =
"org.chromium.webapk.shell_apk.test.canary.Canary";
private Context mContext;
private Context mRemoteContext;
private File mLocalDexDir;
private IDexOptimizerService mDexOptimizerService;
private ServiceConnection mServiceConnection;
/**
* Monitors read files and modified files in the directory passed to the constructor.
*/
private static class FileMonitor extends FileObserver {
public ArrayList<String> mReadPaths = new ArrayList<String>();
public ArrayList<String> mModifiedPaths = new ArrayList<String>();
public FileMonitor(File directory) {
super(directory.getPath());
}
@Override
public void onEvent(int event, String path) {
switch (event) {
case FileObserver.ACCESS:
mReadPaths.add(path);
break;
case FileObserver.CREATE:
case FileObserver.DELETE:
case FileObserver.DELETE_SELF:
case FileObserver.MODIFY:
mModifiedPaths.add(path);
break;
default:
break;
}
}
}
@Override
protected void setUp() {
mContext = getInstrumentation().getTargetContext();
mRemoteContext = getRemoteContext(mContext);
mLocalDexDir = mContext.getDir("dex", Context.MODE_PRIVATE);
if (mLocalDexDir.exists()) {
FileUtils.recursivelyDeleteFile(mLocalDexDir);
if (mLocalDexDir.exists()) {
fail("Could not delete local dex directory.");
}
}
connectToDexOptimizerService();
try {
if (!mDexOptimizerService.deleteDexDirectory()) {
fail("Could not delete remote dex directory.");
}
} catch (RemoteException e) {
e.printStackTrace();
fail("Remote crashed during setup.");
}
}
@Override
public void tearDown() throws Exception {
mContext.unbindService(mServiceConnection);
super.tearDown();
}
/**
* Test that {@DexLoader#load()} can create a ClassLoader from a dex and optimized dex in
* another app's data directory.
*/
@MediumTest
public void testLoadFromRemoteDataDir() {
// Extract the dex file into another app's data directory and optimize the dex.
String remoteDexFilePath = null;
try {
remoteDexFilePath = mDexOptimizerService.extractAndOptimizeDex();
} catch (RemoteException e) {
e.printStackTrace();
fail("Remote crashed.");
}
if (remoteDexFilePath == null) {
fail("Could not extract and optimize dex.");
}
// Check that the Android OS knows about the optimized dex file for
// {@link remoteDexFilePath}.
File remoteDexFile = new File(remoteDexFilePath);
assertFalse(isDexOptNeeded(remoteDexFile));
ClassLoader loader = DexLoader.load(
mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, remoteDexFile, mLocalDexDir);
assertNotNull(loader);
assertTrue(canLoadCanaryClass(loader));
// Check that {@link DexLoader#load()} did not use the fallback path.
assertFalse(mLocalDexDir.exists());
}
/**
* That that {@link DexLoader#load()} falls back to extracting the dex from the APK to the
* local data directory and creating the ClassLoader from the extracted dex if creating the
* ClassLoader from the cached data in the remote Context's data directory fails.
*/
@MediumTest
public void testLoadFromLocalDataDir() {
ClassLoader loader = DexLoader.load(
mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
assertNotNull(loader);
assertTrue(canLoadCanaryClass(loader));
// Check that the dex file was extracted to the local data directory and that a directory
// was created for the optimized dex.
assertTrue(mLocalDexDir.exists());
File[] localDexDirFiles = mLocalDexDir.listFiles();
assertNotNull(localDexDirFiles);
Arrays.sort(localDexDirFiles);
assertEquals(2, localDexDirFiles.length);
assertEquals(DEX_ASSET_NAME, localDexDirFiles[0].getName());
assertFalse(localDexDirFiles[0].isDirectory());
assertEquals("optimized", localDexDirFiles[1].getName());
assertTrue(localDexDirFiles[1].isDirectory());
}
/**
* Test that {@link DexLoader#load()} does not extract the dex file from the APK if the dex file
* was extracted in a previous call to {@link DexLoader#load()}
*/
@MediumTest
public void testPreviouslyLoadedFromLocalDataDir() {
assertTrue(mLocalDexDir.mkdir());
{
// Load dex the first time. This should extract the dex file from the APK's assets and
// generate the optimized dex file.
FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir);
localDexDirMonitor.startWatching();
ClassLoader loader = DexLoader.load(
mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
localDexDirMonitor.stopWatching();
assertNotNull(loader);
assertTrue(canLoadCanaryClass(loader));
assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME));
}
{
// Load dex a second time. We should use the already extracted dex file.
FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir);
localDexDirMonitor.startWatching();
ClassLoader loader = DexLoader.load(
mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
localDexDirMonitor.stopWatching();
// The returned ClassLoader should be valid.
assertNotNull(loader);
assertTrue(canLoadCanaryClass(loader));
// We should not have modified any files and have used the already extracted dex file.
assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
assertTrue(localDexDirMonitor.mModifiedPaths.isEmpty());
}
}
/**
* Test that {@link DexLoader#load()} re-extracts the dex file from the APK after a call to
* {@link DexLoader#deleteCachedDexes()}.
*/
@MediumTest
public void testLoadAfterDeleteCachedDexes() {
assertTrue(mLocalDexDir.mkdir());
{
// Load dex the first time. This should extract the dex file from the APK's assets and
// generate the optimized dex file.
FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir);
localDexDirMonitor.startWatching();
ClassLoader loader = DexLoader.load(
mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
localDexDirMonitor.stopWatching();
assertNotNull(loader);
assertTrue(canLoadCanaryClass(loader));
assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME));
}
DexLoader.deleteCachedDexes(mLocalDexDir);
{
// Load dex a second time.
FileMonitor localDexDirMonitor = new FileMonitor(mLocalDexDir);
localDexDirMonitor.startWatching();
ClassLoader loader = DexLoader.load(
mRemoteContext, DEX_ASSET_NAME, CANARY_CLASS_NAME, null, mLocalDexDir);
localDexDirMonitor.stopWatching();
// The returned ClassLoader should be valid.
assertNotNull(loader);
assertTrue(canLoadCanaryClass(loader));
// We should have re-extracted the dex from the APK's assets.
assertTrue(localDexDirMonitor.mReadPaths.contains(DEX_ASSET_NAME));
assertTrue(localDexDirMonitor.mModifiedPaths.contains(DEX_ASSET_NAME));
}
}
/**
* Connects to the DexOptimizerService.
*/
private void connectToDexOptimizerService() {
Intent intent = new Intent();
intent.setComponent(
new ComponentName(DEX_OPTIMIZER_SERVICE_PACKAGE, DEX_OPTIMIZER_SERVICE_CLASS_NAME));
final CallbackHelper connectedCallback = new CallbackHelper();
mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mDexOptimizerService = IDexOptimizerService.Stub.asInterface(service);
connectedCallback.notifyCalled();
}
@Override
public void onServiceDisconnected(ComponentName name) {}
};
try {
mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
} catch (SecurityException e) {
e.printStackTrace();
fail();
}
try {
connectedCallback.waitForCallback(0);
} catch (Exception e) {
e.printStackTrace();
fail("Could not connect to remote.");
}
}
/**
* Returns the Context of the APK which provides DexOptimizerService.
* @param context The test application's Context.
* @return Context of the APK whcih provide DexOptimizerService.
*/
private Context getRemoteContext(Context context) {
try {
return context.getApplicationContext().createPackageContext(
DEX_OPTIMIZER_SERVICE_PACKAGE,
Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
} catch (NameNotFoundException e) {
e.printStackTrace();
fail("Could not get remote context");
return null;
}
}
/** Returns whether the Android OS thinks that a dex file needs to be re-optimized */
private boolean isDexOptNeeded(File dexFile) {
try {
return DexFile.isDexOptNeeded(dexFile.getPath());
} catch (Exception e) {
e.printStackTrace();
fail();
return false;
}
}
/** Returns whether the ClassLoader can load {@link CANARY_CLASS_NAME} */
private boolean canLoadCanaryClass(ClassLoader loader) {
try {
loader.loadClass(CANARY_CLASS_NAME);
return true;
} catch (Exception e) {
return false;
}
}
}