| Index: chrome/android/webapk/shell_apk/javatests/src/org/chromium/webapk/shell_apk/DexLoaderTest.java | 
| diff --git a/chrome/android/webapk/shell_apk/javatests/src/org/chromium/webapk/shell_apk/DexLoaderTest.java b/chrome/android/webapk/shell_apk/javatests/src/org/chromium/webapk/shell_apk/DexLoaderTest.java | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..e9015944d4976d63a2bddaa21159c7672a97c345 | 
| --- /dev/null | 
| +++ b/chrome/android/webapk/shell_apk/javatests/src/org/chromium/webapk/shell_apk/DexLoaderTest.java | 
| @@ -0,0 +1,332 @@ | 
| +// 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 mRemoteDexFile; | 
| +    private File mLocalDexDir; | 
| +    private IDexOptimizerService mDexOptimizerService; | 
| +    private ServiceConnection mServiceConnection; | 
| + | 
| +    /** | 
| +     * Monitors read files and modified files in the directory passed to the constructor. | 
| +     */ | 
| +    private 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() { | 
| +        mContext.unbindService(mServiceConnection); | 
| +    } | 
| + | 
| +    /** | 
| +     * 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(); | 
| +        Arrays.sort(mLocalDexDir.listFiles()); | 
| +        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() { | 
| +        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() { | 
| +        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; | 
| +        } | 
| +    } | 
| +} | 
|  |