1 // Copyright 2015 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.
5 package org
.chromium
.incrementalinstall
;
7 import android
.content
.Context
;
8 import android
.os
.Build
;
9 import android
.util
.Log
;
12 import java
.io
.FileInputStream
;
13 import java
.io
.FileNotFoundException
;
14 import java
.io
.FileOutputStream
;
15 import java
.io
.IOException
;
16 import java
.util
.List
;
19 * Provides the ability to add native libraries and .dex files to an existing class loader.
20 * Tested with Jellybean MR2 - Marshmellow.
22 final class ClassLoaderPatcher
{
23 private static final String TAG
= "cr.incrementalinstall";
24 private final File mAppFilesSubDir
;
25 private final ClassLoader mClassLoader
;
26 private final Object mLibcoreOs
;
27 private final int mProcessUid
;
28 final boolean mIsPrimaryProcess
;
30 ClassLoaderPatcher(Context context
) throws ReflectiveOperationException
{
32 new File(context
.getApplicationInfo().dataDir
, "incremental-install-files");
33 mClassLoader
= context
.getClassLoader();
34 mLibcoreOs
= Reflect
.getField(Class
.forName("libcore.io.Libcore"), "os");
35 mProcessUid
= (Integer
) Reflect
.invokeMethod(mLibcoreOs
, "getuid");
36 mIsPrimaryProcess
= context
.getApplicationInfo().uid
== mProcessUid
;
37 Log
.i(TAG
, "uid=" + mProcessUid
+ " (isPrimary=" + mIsPrimaryProcess
+ ")");
41 * Loads all dex files within |dexDir| into the app's ClassLoader.
43 void loadDexFiles(File dexDir
) throws ReflectiveOperationException
, FileNotFoundException
{
44 Log
.i(TAG
, "Installing dex files from: " + dexDir
);
45 File
[] dexFilesArr
= dexDir
.listFiles();
46 if (dexFilesArr
== null) {
47 throw new FileNotFoundException("Dex dir does not exist: " + dexDir
);
49 // The optimized dex files will be owned by this process' user.
50 // Store them within the app's data dir rather than on /data/local/tmp
51 // so that they are still deleted (by the OS) when we uninstall
52 // (even on a non-rooted device).
53 File incrementalDexesDir
= new File(mAppFilesSubDir
, "optimized-dexes");
54 File isolatedDexesDir
= new File(mAppFilesSubDir
, "isolated-dexes");
57 if (mIsPrimaryProcess
) {
58 ensureAppFilesSubDirExists();
59 // Allows isolated processes to access the same files.
60 incrementalDexesDir
.mkdir();
61 incrementalDexesDir
.setReadable(true, false);
62 incrementalDexesDir
.setExecutable(true, false);
63 // Create a directory for isolated processes to create directories in.
64 isolatedDexesDir
.mkdir();
65 isolatedDexesDir
.setWritable(true, false);
66 isolatedDexesDir
.setExecutable(true, false);
68 optimizedDir
= incrementalDexesDir
;
70 // There is a UID check of the directory in dalvik.system.DexFile():
71 // https://android.googlesource.com/platform/libcore/+/45e0260/dalvik/src/main/java/dalvik/system/DexFile.java#101
72 // Rather than have each isolated process run DexOpt though, we use
73 // symlinks within the directory to point at the browser process'
74 // optimized dex files.
75 optimizedDir
= new File(isolatedDexesDir
, "isolated-" + mProcessUid
);
77 // Always wipe it out and re-create for simplicity.
78 Log
.i(TAG
, "Creating dex file symlinks for isolated process");
79 for (File f
: optimizedDir
.listFiles()) {
82 for (File f
: incrementalDexesDir
.listFiles()) {
83 String to
= "../../" + incrementalDexesDir
.getName() + "/" + f
.getName();
84 File from
= new File(optimizedDir
, f
.getName());
85 createSymlink(to
, from
);
89 Log
.i(TAG
, "Code cache dir: " + optimizedDir
);
90 // TODO(agrieve): Might need to record classpath ordering if we ever have duplicate
91 // class names (since then order will matter here).
92 Log
.i(TAG
, "Loading " + dexFilesArr
.length
+ " dex files");
94 Object dexPathList
= Reflect
.getField(mClassLoader
, "pathList");
95 Object
[] dexElements
= (Object
[]) Reflect
.getField(dexPathList
, "dexElements");
96 Object
[] additionalElements
= makeDexElements(dexFilesArr
, optimizedDir
);
98 dexPathList
, "dexElements", Reflect
.concatArrays(dexElements
, additionalElements
));
102 * Sets up all libraries within |libDir| to be loadable by System.loadLibrary().
104 void importNativeLibs(File libDir
) throws ReflectiveOperationException
, IOException
{
105 Log
.i(TAG
, "Importing native libraries from: " + libDir
);
106 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.M
) {
107 libDir
= prepareNativeLibsAndroidM(libDir
);
109 addNativeLibrarySearchPath(libDir
);
113 * Primary process: Copies native libraries into the app's data directory
114 * Other processes: Waits for primary process to finish copying.
116 private File
prepareNativeLibsAndroidM(File libDir
) throws IOException
{
117 File localLibsDir
= new File(mAppFilesSubDir
, "lib");
118 File copyLibsLockFile
= new File(mAppFilesSubDir
, "libcopy.lock");
119 // Due to a new SELinux policy, all libs must be copied into the app's
120 // data directory first.
121 // https://code.google.com/p/android/issues/detail?id=79480
122 if (mIsPrimaryProcess
) {
123 LockFile lockFile
= LockFile
.acquireRuntimeLock(copyLibsLockFile
);
124 if (lockFile
== null) {
125 LockFile
.waitForRuntimeLock(copyLibsLockFile
, 10 * 1000);
128 ensureAppFilesSubDirExists();
129 localLibsDir
.mkdir();
130 localLibsDir
.setReadable(true, false);
131 localLibsDir
.setExecutable(true, false);
132 copyChangedFiles(libDir
, localLibsDir
);
138 // TODO: Work around this issue by using APK splits to install each dex / lib.
139 throw new RuntimeException("Incremental install does not work on Android M+ "
140 + "with isolated processes. Use the gn arg:\n"
141 + " disable_incremental_isolated_processes=true\n"
147 @SuppressWarnings("unchecked")
148 private void addNativeLibrarySearchPath(File nativeLibDir
) throws ReflectiveOperationException
{
149 Object dexPathList
= Reflect
.getField(mClassLoader
, "pathList");
150 Object currentDirs
= Reflect
.getField(dexPathList
, "nativeLibraryDirectories");
151 File
[] newDirs
= new File
[] { nativeLibDir
};
152 // Switched from an array to an ArrayList in Lollipop.
153 if (currentDirs
instanceof List
) {
154 List
<File
> dirsAsList
= (List
<File
>) currentDirs
;
155 dirsAsList
.add(nativeLibDir
);
157 File
[] dirsAsArray
= (File
[]) currentDirs
;
158 Reflect
.setField(dexPathList
, "nativeLibraryDirectories",
159 Reflect
.concatArrays(dirsAsArray
, newDirs
));
162 Object
[] nativeLibraryPathElements
;
164 nativeLibraryPathElements
=
165 (Object
[]) Reflect
.getField(dexPathList
, "nativeLibraryPathElements");
166 } catch (NoSuchFieldException e
) {
167 // This field doesn't exist pre-M.
170 Object
[] additionalElements
= makeNativePathElements(newDirs
);
172 dexPathList
, "nativeLibraryPathElements",
173 Reflect
.concatArrays(nativeLibraryPathElements
, additionalElements
));
176 private static void copyChangedFiles(File srcDir
, File dstDir
) throws IOException
{
177 // No need to delete stale libs since libraries are loaded explicitly.
178 for (File f
: srcDir
.listFiles()) {
179 // Note: Tried using hardlinks, but resulted in EACCES exceptions.
180 File dest
= new File(dstDir
, f
.getName());
181 copyIfModified(f
, dest
);
185 private static void copyIfModified(File src
, File dest
) throws IOException
{
186 long lastModified
= src
.lastModified();
187 if (!dest
.exists() || dest
.lastModified() != lastModified
) {
188 Log
.i(TAG
, "Copying " + src
+ " -> " + dest
);
189 FileInputStream istream
= new FileInputStream(src
);
190 FileOutputStream ostream
= new FileOutputStream(dest
);
191 ostream
.getChannel().transferFrom(istream
.getChannel(), 0, istream
.getChannel().size());
194 dest
.setReadable(true, false);
195 dest
.setExecutable(true, false);
196 dest
.setLastModified(lastModified
);
200 private void ensureAppFilesSubDirExists() {
201 mAppFilesSubDir
.mkdir();
202 mAppFilesSubDir
.setExecutable(true, false);
205 private void createSymlink(String to
, File from
) throws ReflectiveOperationException
{
206 Reflect
.invokeMethod(mLibcoreOs
, "symlink", to
, from
.getAbsolutePath());
209 private static Object
[] makeNativePathElements(File
[] paths
)
210 throws ReflectiveOperationException
{
211 Class
<?
> entryClazz
= Class
.forName("dalvik.system.DexPathList$Element");
212 Object
[] entries
= new Object
[paths
.length
];
213 for (int i
= 0; i
< paths
.length
; ++i
) {
214 entries
[i
] = Reflect
.newInstance(entryClazz
, paths
[i
], true, null, null);
219 private static Object
[] makeDexElements(File
[] files
, File optimizedDirectory
)
220 throws ReflectiveOperationException
{
221 Class
<?
> entryClazz
= Class
.forName("dalvik.system.DexPathList$Element");
222 Class
<?
> clazz
= Class
.forName("dalvik.system.DexPathList");
223 Object
[] entries
= new Object
[files
.length
];
224 File emptyDir
= new File("");
225 for (int i
= 0; i
< files
.length
; ++i
) {
226 File file
= files
[i
];
227 Object dexFile
= Reflect
.invokeMethod(clazz
, "loadDexFile", file
, optimizedDirectory
);
228 entries
[i
] = Reflect
.newInstance(entryClazz
, emptyDir
, false, file
, dexFile
);