diff options
author | Alan Viverette <alanv@google.com> | 2018-03-08 15:27:11 -0500 |
---|---|---|
committer | Alan Viverette <alanv@google.com> | 2018-03-09 10:59:13 -0500 |
commit | 475db106115d4f25f0e41612a2a71a0cfe83ab38 (patch) | |
tree | b2f362e1b15621a2247fd2c3be0e4607d6784290 /library/src/androidx/multidex | |
parent | eb8d5626ff8b7781e3a2ec6c3990785697adb99f (diff) | |
download | multidex-475db106115d4f25f0e41612a2a71a0cfe83ab38.tar.gz |
Migrate multidex to androidxandroid-9.0.0_r47android-9.0.0_r46android-9.0.0_r45android-9.0.0_r44android-9.0.0_r43android-9.0.0_r42android-9.0.0_r41android-9.0.0_r40android-9.0.0_r39android-9.0.0_r38android-9.0.0_r37android-9.0.0_r36android-9.0.0_r35android-9.0.0_r34android-9.0.0_r33android-9.0.0_r32android-9.0.0_r31android-9.0.0_r30android-9.0.0_r22android-9.0.0_r21android-9.0.0_r20android-9.0.0_r19android-9.0.0_r16android-9.0.0_r12android-9.0.0_r11pie-qpr3-s1-releasepie-qpr3-releasepie-qpr3-b-releasepie-qpr2-releasepie-qpr1-s3-releasepie-qpr1-s2-releasepie-qpr1-s1-releasepie-qpr1-releasepie-dr1-releasepie-dr1-devpie-devpie-b4s4-releasepie-b4s4-dev
Bug: 74397601
Test: make
Change-Id: Iddea6f92cc7796125cf4d1ba86cf9b7425daef72
Diffstat (limited to 'library/src/androidx/multidex')
-rw-r--r-- | library/src/androidx/multidex/MultiDex.java | 765 | ||||
-rw-r--r-- | library/src/androidx/multidex/MultiDexApplication.java | 41 | ||||
-rw-r--r-- | library/src/androidx/multidex/MultiDexExtractor.java | 427 | ||||
-rw-r--r-- | library/src/androidx/multidex/ZipUtil.java | 125 |
4 files changed, 1358 insertions, 0 deletions
diff --git a/library/src/androidx/multidex/MultiDex.java b/library/src/androidx/multidex/MultiDex.java new file mode 100644 index 0000000..69ff889 --- /dev/null +++ b/library/src/androidx/multidex/MultiDex.java @@ -0,0 +1,765 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.multidex; + +import android.app.Application; +import android.app.Instrumentation; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.os.Build; +import android.util.Log; +import dalvik.system.DexFile; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipFile; + +/** + * MultiDex patches {@link Context#getClassLoader() the application context class + * loader} in order to load classes from more than one dex file. The primary + * {@code classes.dex} must contain the classes necessary for calling this + * class methods. Secondary dex files named classes2.dex, classes3.dex... found + * in the application apk will be added to the classloader after first call to + * {@link #install(Context)}. + * + * <p/> + * This library provides compatibility for platforms with API level 4 through 20. This library does + * nothing on newer versions of the platform which provide built-in support for secondary dex files. + */ +public final class MultiDex { + + static final String TAG = "MultiDex"; + + private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes"; + + private static final String CODE_CACHE_NAME = "code_cache"; + + private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes"; + + private static final int MAX_SUPPORTED_SDK_VERSION = 20; + + private static final int MIN_SDK_VERSION = 4; + + private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2; + + private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1; + + private static final String NO_KEY_PREFIX = ""; + + private static final Set<File> installedApk = new HashSet<File>(); + + private static final boolean IS_VM_MULTIDEX_CAPABLE = + isVMMultidexCapable(System.getProperty("java.vm.version")); + + private MultiDex() {} + + /** + * Patches the application context class loader by appending extra dex files + * loaded from the application apk. This method should be called in the + * attachBaseContext of your {@link Application}, see + * {@link MultiDexApplication} for more explanation and an example. + * + * @param context application context. + * @throws RuntimeException if an error occurred preventing the classloader + * extension. + */ + public static void install(Context context) { + Log.i(TAG, "Installing application"); + if (IS_VM_MULTIDEX_CAPABLE) { + Log.i(TAG, "VM has multidex support, MultiDex support library is disabled."); + return; + } + + if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) { + throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT + + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + "."); + } + + try { + ApplicationInfo applicationInfo = getApplicationInfo(context); + if (applicationInfo == null) { + Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:" + + " MultiDex support library is disabled."); + return; + } + + doInstallation(context, + new File(applicationInfo.sourceDir), + new File(applicationInfo.dataDir), + CODE_CACHE_SECONDARY_FOLDER_NAME, + NO_KEY_PREFIX, + true); + + } catch (Exception e) { + Log.e(TAG, "MultiDex installation failure", e); + throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ")."); + } + Log.i(TAG, "install done"); + } + + /** + * Patches the instrumentation context class loader by appending extra dex files + * loaded from the instrumentation apk and the application apk. This method should be called in + * the onCreate of your {@link Instrumentation}, see + * {@link com.android.test.runner.MultiDexTestRunner} for an example. + * + * @param instrumentationContext instrumentation context. + * @param targetContext target application context. + * @throws RuntimeException if an error occurred preventing the classloader + * extension. + */ + public static void installInstrumentation(Context instrumentationContext, + Context targetContext) { + Log.i(TAG, "Installing instrumentation"); + + if (IS_VM_MULTIDEX_CAPABLE) { + Log.i(TAG, "VM has multidex support, MultiDex support library is disabled."); + return; + } + + if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) { + throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT + + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + "."); + } + try { + + ApplicationInfo instrumentationInfo = getApplicationInfo(instrumentationContext); + if (instrumentationInfo == null) { + Log.i(TAG, "No ApplicationInfo available for instrumentation, i.e. running on a" + + " test Context: MultiDex support library is disabled."); + return; + } + + ApplicationInfo applicationInfo = getApplicationInfo(targetContext); + if (applicationInfo == null) { + Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:" + + " MultiDex support library is disabled."); + return; + } + + String instrumentationPrefix = instrumentationContext.getPackageName() + "."; + + File dataDir = new File(applicationInfo.dataDir); + + doInstallation(targetContext, + new File(instrumentationInfo.sourceDir), + dataDir, + instrumentationPrefix + CODE_CACHE_SECONDARY_FOLDER_NAME, + instrumentationPrefix, + false); + + doInstallation(targetContext, + new File(applicationInfo.sourceDir), + dataDir, + CODE_CACHE_SECONDARY_FOLDER_NAME, + NO_KEY_PREFIX, + false); + } catch (Exception e) { + Log.e(TAG, "MultiDex installation failure", e); + throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ")."); + } + Log.i(TAG, "Installation done"); + } + + /** + * @param mainContext context used to get filesDir, to save preference and to get the + * classloader to patch. + * @param sourceApk Apk file. + * @param dataDir data directory to use for code cache simulation. + * @param secondaryFolderName name of the folder for storing extractions. + * @param prefsKeyPrefix prefix of all stored preference keys. + * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction + * if a possibly recoverable exception occurs during classloader patching. + */ + private static void doInstallation(Context mainContext, File sourceApk, File dataDir, + String secondaryFolderName, String prefsKeyPrefix, + boolean reinstallOnPatchRecoverableException) throws IOException, + IllegalArgumentException, IllegalAccessException, NoSuchFieldException, + InvocationTargetException, NoSuchMethodException, SecurityException, + ClassNotFoundException, InstantiationException { + synchronized (installedApk) { + if (installedApk.contains(sourceApk)) { + return; + } + installedApk.add(sourceApk); + + if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) { + Log.w(TAG, "MultiDex is not guaranteed to work in SDK version " + + Build.VERSION.SDK_INT + ": SDK version higher than " + + MAX_SUPPORTED_SDK_VERSION + " should be backed by " + + "runtime with built-in multidex capabilty but it's not the " + + "case here: java.vm.version=\"" + + System.getProperty("java.vm.version") + "\""); + } + + /* The patched class loader is expected to be a descendant of + * dalvik.system.BaseDexClassLoader. We modify its + * dalvik.system.DexPathList pathList field to append additional DEX + * file entries. + */ + ClassLoader loader; + try { + loader = mainContext.getClassLoader(); + } catch (RuntimeException e) { + /* Ignore those exceptions so that we don't break tests relying on Context like + * a android.test.mock.MockContext or a android.content.ContextWrapper with a + * null base Context. + */ + Log.w(TAG, "Failure while trying to obtain Context class loader. " + + "Must be running in test mode. Skip patching.", e); + return; + } + if (loader == null) { + // Note, the context class loader is null when running Robolectric tests. + Log.e(TAG, + "Context class loader is null. Must be running in test mode. " + + "Skip patching."); + return; + } + + try { + clearOldDexDir(mainContext); + } catch (Throwable t) { + Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, " + + "continuing without cleaning.", t); + } + + File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName); + // MultiDexExtractor is taking the file lock and keeping it until it is closed. + // Keep it open during installSecondaryDexes and through forced extraction to ensure no + // extraction or optimizing dexopt is running in parallel. + MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir); + IOException closeException = null; + try { + List<? extends File> files = + extractor.load(mainContext, prefsKeyPrefix, false); + try { + installSecondaryDexes(loader, dexDir, files); + // Some IOException causes may be fixed by a clean extraction. + } catch (IOException e) { + if (!reinstallOnPatchRecoverableException) { + throw e; + } + Log.w(TAG, "Failed to install extracted secondary dex files, retrying with " + + "forced extraction", e); + files = extractor.load(mainContext, prefsKeyPrefix, true); + installSecondaryDexes(loader, dexDir, files); + } + } finally { + try { + extractor.close(); + } catch (IOException e) { + // Delay throw of close exception to ensure we don't override some exception + // thrown during the try block. + closeException = e; + } + } + if (closeException != null) { + throw closeException; + } + } + } + + private static ApplicationInfo getApplicationInfo(Context context) { + try { + /* Due to package install races it is possible for a process to be started from an old + * apk even though that apk has been replaced. Querying for ApplicationInfo by package + * name may return information for the new apk, leading to a runtime with the old main + * dex file and new secondary dex files. This leads to various problems like + * ClassNotFoundExceptions. Using context.getApplicationInfo() should result in the + * process having a consistent view of the world (even if it is of the old world). The + * package install races are eventually resolved and old processes are killed. + */ + return context.getApplicationInfo(); + } catch (RuntimeException e) { + /* Ignore those exceptions so that we don't break tests relying on Context like + * a android.test.mock.MockContext or a android.content.ContextWrapper with a null + * base Context. + */ + Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " + + "Must be running in test mode. Skip patching.", e); + return null; + } + } + + /** + * Identifies if the current VM has a native support for multidex, meaning there is no need for + * additional installation by this library. + * @return true if the VM handles multidex + */ + /* package visible for test */ + static boolean isVMMultidexCapable(String versionString) { + boolean isMultidexCapable = false; + if (versionString != null) { + Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString); + if (matcher.matches()) { + try { + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR) + || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR) + && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR)); + } catch (NumberFormatException e) { + // let isMultidexCapable be false + } + } + } + Log.i(TAG, "VM with version " + versionString + + (isMultidexCapable ? + " has multidex support" : + " does not have multidex support")); + return isMultidexCapable; + } + + private static void installSecondaryDexes(ClassLoader loader, File dexDir, + List<? extends File> files) + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, + InvocationTargetException, NoSuchMethodException, IOException, SecurityException, + ClassNotFoundException, InstantiationException { + if (!files.isEmpty()) { + if (Build.VERSION.SDK_INT >= 19) { + V19.install(loader, files, dexDir); + } else if (Build.VERSION.SDK_INT >= 14) { + V14.install(loader, files); + } else { + V4.install(loader, files); + } + } + } + + /** + * Locates a given field anywhere in the class inheritance hierarchy. + * + * @param instance an object to search the field into. + * @param name field name + * @return a field object + * @throws NoSuchFieldException if the field cannot be located + */ + private static Field findField(Object instance, String name) throws NoSuchFieldException { + for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + Field field = clazz.getDeclaredField(name); + + + if (!field.isAccessible()) { + field.setAccessible(true); + } + + return field; + } catch (NoSuchFieldException e) { + // ignore and search next + } + } + + throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass()); + } + + /** + * Locates a given method anywhere in the class inheritance hierarchy. + * + * @param instance an object to search the method into. + * @param name method name + * @param parameterTypes method parameter types + * @return a method object + * @throws NoSuchMethodException if the method cannot be located + */ + private static Method findMethod(Object instance, String name, Class<?>... parameterTypes) + throws NoSuchMethodException { + for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + Method method = clazz.getDeclaredMethod(name, parameterTypes); + + + if (!method.isAccessible()) { + method.setAccessible(true); + } + + return method; + } catch (NoSuchMethodException e) { + // ignore and search next + } + } + + throw new NoSuchMethodException("Method " + name + " with parameters " + + Arrays.asList(parameterTypes) + " not found in " + instance.getClass()); + } + + /** + * Replace the value of a field containing a non null array, by a new array containing the + * elements of the original array plus the elements of extraElements. + * @param instance the instance whose field is to be modified. + * @param fieldName the field to modify. + * @param extraElements elements to append at the end of the array. + */ + private static void expandFieldArray(Object instance, String fieldName, + Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, + IllegalAccessException { + Field jlrField = findField(instance, fieldName); + Object[] original = (Object[]) jlrField.get(instance); + Object[] combined = (Object[]) Array.newInstance( + original.getClass().getComponentType(), original.length + extraElements.length); + System.arraycopy(original, 0, combined, 0, original.length); + System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); + jlrField.set(instance, combined); + } + + private static void clearOldDexDir(Context context) throws Exception { + File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME); + if (dexDir.isDirectory()) { + Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ")."); + File[] files = dexDir.listFiles(); + if (files == null) { + Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); + return; + } + for (File oldFile : files) { + Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + + oldFile.length()); + if (!oldFile.delete()) { + Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); + } else { + Log.i(TAG, "Deleted old file " + oldFile.getPath()); + } + } + if (!dexDir.delete()) { + Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath()); + } else { + Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath()); + } + } + } + + private static File getDexDir(Context context, File dataDir, String secondaryFolderName) + throws IOException { + File cache = new File(dataDir, CODE_CACHE_NAME); + try { + mkdirChecked(cache); + } catch (IOException e) { + /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless + * files on disk if the device ever updates to android 5+. But since this seems to + * happen only on some devices running android 2, this should cause no pollution. + */ + cache = new File(context.getFilesDir(), CODE_CACHE_NAME); + mkdirChecked(cache); + } + File dexDir = new File(cache, secondaryFolderName); + mkdirChecked(dexDir); + return dexDir; + } + + private static void mkdirChecked(File dir) throws IOException { + dir.mkdir(); + if (!dir.isDirectory()) { + File parent = dir.getParentFile(); + if (parent == null) { + Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null."); + } else { + Log.e(TAG, "Failed to create dir " + dir.getPath() + + ". parent file is a dir " + parent.isDirectory() + + ", a file " + parent.isFile() + + ", exists " + parent.exists() + + ", readable " + parent.canRead() + + ", writable " + parent.canWrite()); + } + throw new IOException("Failed to create directory " + dir.getPath()); + } + } + + /** + * Installer for platform versions 19. + */ + private static final class V19 { + + static void install(ClassLoader loader, + List<? extends File> additionalClassPathEntries, + File optimizedDirectory) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, InvocationTargetException, NoSuchMethodException, + IOException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.BaseDexClassLoader. We modify its + * dalvik.system.DexPathList pathList field to append additional DEX + * file entries. + */ + Field pathListField = findField(loader, "pathList"); + Object dexPathList = pathListField.get(loader); + ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); + expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, + new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, + suppressedExceptions)); + if (suppressedExceptions.size() > 0) { + for (IOException e : suppressedExceptions) { + Log.w(TAG, "Exception in makeDexElement", e); + } + Field suppressedExceptionsField = + findField(dexPathList, "dexElementsSuppressedExceptions"); + IOException[] dexElementsSuppressedExceptions = + (IOException[]) suppressedExceptionsField.get(dexPathList); + + if (dexElementsSuppressedExceptions == null) { + dexElementsSuppressedExceptions = + suppressedExceptions.toArray( + new IOException[suppressedExceptions.size()]); + } else { + IOException[] combined = + new IOException[suppressedExceptions.size() + + dexElementsSuppressedExceptions.length]; + suppressedExceptions.toArray(combined); + System.arraycopy(dexElementsSuppressedExceptions, 0, combined, + suppressedExceptions.size(), dexElementsSuppressedExceptions.length); + dexElementsSuppressedExceptions = combined; + } + + suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions); + + IOException exception = new IOException("I/O exception during makeDexElement"); + exception.initCause(suppressedExceptions.get(0)); + throw exception; + } + } + + /** + * A wrapper around + * {@code private static final dalvik.system.DexPathList#makeDexElements}. + */ + private static Object[] makeDexElements( + Object dexPathList, ArrayList<File> files, File optimizedDirectory, + ArrayList<IOException> suppressedExceptions) + throws IllegalAccessException, InvocationTargetException, + NoSuchMethodException { + Method makeDexElements = + findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, + ArrayList.class); + + return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, + suppressedExceptions); + } + } + + /** + * Installer for platform versions 14, 15, 16, 17 and 18. + */ + private static final class V14 { + + private interface ElementConstructor { + Object newInstance(File file, DexFile dex) + throws IllegalArgumentException, InstantiationException, + IllegalAccessException, InvocationTargetException, IOException; + } + + /** + * Applies for ICS and early JB (initial release and MR1). + */ + private static class ICSElementConstructor implements ElementConstructor { + private final Constructor<?> elementConstructor; + + ICSElementConstructor(Class<?> elementClass) + throws SecurityException, NoSuchMethodException { + elementConstructor = + elementClass.getConstructor(File.class, ZipFile.class, DexFile.class); + elementConstructor.setAccessible(true); + } + + @Override + public Object newInstance(File file, DexFile dex) + throws IllegalArgumentException, InstantiationException, + IllegalAccessException, InvocationTargetException, IOException { + return elementConstructor.newInstance(file, new ZipFile(file), dex); + } + } + + /** + * Applies for some intermediate JB (MR1.1). + * + * See Change-Id: I1a5b5d03572601707e1fb1fd4424c1ae2fd2217d + */ + private static class JBMR11ElementConstructor implements ElementConstructor { + private final Constructor<?> elementConstructor; + + JBMR11ElementConstructor(Class<?> elementClass) + throws SecurityException, NoSuchMethodException { + elementConstructor = elementClass + .getConstructor(File.class, File.class, DexFile.class); + elementConstructor.setAccessible(true); + } + + @Override + public Object newInstance(File file, DexFile dex) + throws IllegalArgumentException, InstantiationException, + IllegalAccessException, InvocationTargetException { + return elementConstructor.newInstance(file, file, dex); + } + } + + /** + * Applies for latest JB (MR2). + * + * See Change-Id: Iec4dca2244db9c9c793ac157e258fd61557a7a5d + */ + private static class JBMR2ElementConstructor implements ElementConstructor { + private final Constructor<?> elementConstructor; + + JBMR2ElementConstructor(Class<?> elementClass) + throws SecurityException, NoSuchMethodException { + elementConstructor = elementClass + .getConstructor(File.class, Boolean.TYPE, File.class, DexFile.class); + elementConstructor.setAccessible(true); + } + + @Override + public Object newInstance(File file, DexFile dex) + throws IllegalArgumentException, InstantiationException, + IllegalAccessException, InvocationTargetException { + return elementConstructor.newInstance(file, Boolean.FALSE, file, dex); + } + } + + private static final int EXTRACTED_SUFFIX_LENGTH = + MultiDexExtractor.EXTRACTED_SUFFIX.length(); + + private final ElementConstructor elementConstructor; + + static void install(ClassLoader loader, + List<? extends File> additionalClassPathEntries) + throws IOException, SecurityException, IllegalArgumentException, + ClassNotFoundException, NoSuchMethodException, InstantiationException, + IllegalAccessException, InvocationTargetException, NoSuchFieldException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.BaseDexClassLoader. We modify its + * dalvik.system.DexPathList pathList field to append additional DEX + * file entries. + */ + Field pathListField = findField(loader, "pathList"); + Object dexPathList = pathListField.get(loader); + Object[] elements = new V14().makeDexElements(additionalClassPathEntries); + try { + expandFieldArray(dexPathList, "dexElements", elements); + } catch (NoSuchFieldException e) { + // dexElements was renamed pathElements for a short period during JB development, + // eventually it was renamed back shortly after. + Log.w(TAG, "Failed find field 'dexElements' attempting 'pathElements'", e); + expandFieldArray(dexPathList, "pathElements", elements); + } + } + + private V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException { + ElementConstructor constructor; + Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element"); + try { + constructor = new ICSElementConstructor(elementClass); + } catch (NoSuchMethodException e1) { + try { + constructor = new JBMR11ElementConstructor(elementClass); + } catch (NoSuchMethodException e2) { + constructor = new JBMR2ElementConstructor(elementClass); + } + } + this.elementConstructor = constructor; + } + + /** + * An emulation of {@code private static final dalvik.system.DexPathList#makeDexElements} + * accepting only extracted secondary dex files. + * OS version is catching IOException and just logging some of them, this version is letting + * them through. + */ + private Object[] makeDexElements(List<? extends File> files) + throws IOException, SecurityException, IllegalArgumentException, + InstantiationException, IllegalAccessException, InvocationTargetException { + Object[] elements = new Object[files.size()]; + for (int i = 0; i < elements.length; i++) { + File file = files.get(i); + elements[i] = elementConstructor.newInstance( + file, + DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0)); + } + return elements; + } + + /** + * Converts a zip file path of an extracted secondary dex to an output file path for an + * associated optimized dex file. + */ + private static String optimizedPathFor(File path) { + // Any reproducible name ending with ".dex" should do but lets keep the same name + // as DexPathList.optimizedPathFor + + File optimizedDirectory = path.getParentFile(); + String fileName = path.getName(); + String optimizedFileName = + fileName.substring(0, fileName.length() - EXTRACTED_SUFFIX_LENGTH) + + MultiDexExtractor.DEX_SUFFIX; + File result = new File(optimizedDirectory, optimizedFileName); + return result.getPath(); + } + } + + /** + * Installer for platform versions 4 to 13. + */ + private static final class V4 { + static void install(ClassLoader loader, + List<? extends File> additionalClassPathEntries) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, IOException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.DexClassLoader. We modify its + * fields mPaths, mFiles, mZips and mDexs to append additional DEX + * file entries. + */ + int extraSize = additionalClassPathEntries.size(); + + Field pathField = findField(loader, "path"); + + StringBuilder path = new StringBuilder((String) pathField.get(loader)); + String[] extraPaths = new String[extraSize]; + File[] extraFiles = new File[extraSize]; + ZipFile[] extraZips = new ZipFile[extraSize]; + DexFile[] extraDexs = new DexFile[extraSize]; + for (ListIterator<? extends File> iterator = additionalClassPathEntries.listIterator(); + iterator.hasNext();) { + File additionalEntry = iterator.next(); + String entryPath = additionalEntry.getAbsolutePath(); + path.append(':').append(entryPath); + int index = iterator.previousIndex(); + extraPaths[index] = entryPath; + extraFiles[index] = additionalEntry; + extraZips[index] = new ZipFile(additionalEntry); + extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); + } + + pathField.set(loader, path.toString()); + expandFieldArray(loader, "mPaths", extraPaths); + expandFieldArray(loader, "mFiles", extraFiles); + expandFieldArray(loader, "mZips", extraZips); + expandFieldArray(loader, "mDexs", extraDexs); + } + } + +} diff --git a/library/src/androidx/multidex/MultiDexApplication.java b/library/src/androidx/multidex/MultiDexApplication.java new file mode 100644 index 0000000..4c759e3 --- /dev/null +++ b/library/src/androidx/multidex/MultiDexApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.multidex; + +import android.app.Application; +import android.content.Context; + +/** + * Minimal MultiDex capable application. To use the legacy multidex library there is 3 possibility: + * <ul> + * <li>Declare this class as the application in your AndroidManifest.xml.</li> + * <li>Have your {@link Application} extends this class.</li> + * <li>Have your {@link Application} override attachBaseContext starting with<br> + * <code> + protected void attachBaseContext(Context base) {<br> + super.attachBaseContext(base);<br> + MultiDex.install(this); + </code></li> + * <ul> + */ +public class MultiDexApplication extends Application { + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + MultiDex.install(this); + } +} diff --git a/library/src/androidx/multidex/MultiDexExtractor.java b/library/src/androidx/multidex/MultiDexExtractor.java new file mode 100644 index 0000000..2b96113 --- /dev/null +++ b/library/src/androidx/multidex/MultiDexExtractor.java @@ -0,0 +1,427 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.multidex; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.util.Log; +import java.io.BufferedOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +/** + * Exposes application secondary dex files as files in the application data + * directory. + * {@link MultiDexExtractor} is taking the file lock in the dex dir on creation and release it + * during close. + */ +final class MultiDexExtractor implements Closeable { + + /** + * Zip file containing one secondary dex file. + */ + private static class ExtractedDex extends File { + public long crc = NO_VALUE; + + public ExtractedDex(File dexDir, String fileName) { + super(dexDir, fileName); + } + } + + private static final String TAG = MultiDex.TAG; + + /** + * We look for additional dex files named {@code classes2.dex}, + * {@code classes3.dex}, etc. + */ + private static final String DEX_PREFIX = "classes"; + static final String DEX_SUFFIX = ".dex"; + + private static final String EXTRACTED_NAME_EXT = ".classes"; + static final String EXTRACTED_SUFFIX = ".zip"; + private static final int MAX_EXTRACT_ATTEMPTS = 3; + + private static final String PREFS_FILE = "multidex.version"; + private static final String KEY_TIME_STAMP = "timestamp"; + private static final String KEY_CRC = "crc"; + private static final String KEY_DEX_NUMBER = "dex.number"; + private static final String KEY_DEX_CRC = "dex.crc."; + private static final String KEY_DEX_TIME = "dex.time."; + + /** + * Size of reading buffers. + */ + private static final int BUFFER_SIZE = 0x4000; + /* Keep value away from 0 because it is a too probable time stamp value */ + private static final long NO_VALUE = -1L; + + private static final String LOCK_FILENAME = "MultiDex.lock"; + private final File sourceApk; + private final long sourceCrc; + private final File dexDir; + private final RandomAccessFile lockRaf; + private final FileChannel lockChannel; + private final FileLock cacheLock; + + MultiDexExtractor(File sourceApk, File dexDir) throws IOException { + Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")"); + this.sourceApk = sourceApk; + this.dexDir = dexDir; + sourceCrc = getZipCrc(sourceApk); + File lockFile = new File(dexDir, LOCK_FILENAME); + lockRaf = new RandomAccessFile(lockFile, "rw"); + try { + lockChannel = lockRaf.getChannel(); + try { + Log.i(TAG, "Blocking on lock " + lockFile.getPath()); + cacheLock = lockChannel.lock(); + } catch (IOException | RuntimeException | Error e) { + closeQuietly(lockChannel); + throw e; + } + Log.i(TAG, lockFile.getPath() + " locked"); + } catch (IOException | RuntimeException | Error e) { + closeQuietly(lockRaf); + throw e; + } + } + + /** + * Extracts application secondary dexes into files in the application data + * directory. + * + * @return a list of files that were created. The list may be empty if there + * are no secondary dex files. Never return null. + * @throws IOException if encounters a problem while reading or writing + * secondary dex files + */ + List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) + throws IOException { + Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " + + prefsKeyPrefix + ")"); + + if (!cacheLock.isValid()) { + throw new IllegalStateException("MultiDexExtractor was closed"); + } + + List<ExtractedDex> files; + if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) { + try { + files = loadExistingExtractions(context, prefsKeyPrefix); + } catch (IOException ioe) { + Log.w(TAG, "Failed to reload existing extracted secondary dex files," + + " falling back to fresh extraction", ioe); + files = performExtractions(); + putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc, + files); + } + } else { + if (forceReload) { + Log.i(TAG, "Forced extraction must be performed."); + } else { + Log.i(TAG, "Detected that extraction must be performed."); + } + files = performExtractions(); + putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc, + files); + } + + Log.i(TAG, "load found " + files.size() + " secondary dex files"); + return files; + } + + @Override + public void close() throws IOException { + cacheLock.release(); + lockChannel.close(); + lockRaf.close(); + } + + /** + * Load previously extracted secondary dex files. Should be called only while owning the lock on + * {@link #LOCK_FILENAME}. + */ + private List<ExtractedDex> loadExistingExtractions( + Context context, + String prefsKeyPrefix) + throws IOException { + Log.i(TAG, "loading existing secondary dex files"); + + final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; + SharedPreferences multiDexPreferences = getMultiDexPreferences(context); + int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1); + final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1); + + for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) { + String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; + ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName); + if (extractedFile.isFile()) { + extractedFile.crc = getZipCrc(extractedFile); + long expectedCrc = multiDexPreferences.getLong( + prefsKeyPrefix + KEY_DEX_CRC + secondaryNumber, NO_VALUE); + long expectedModTime = multiDexPreferences.getLong( + prefsKeyPrefix + KEY_DEX_TIME + secondaryNumber, NO_VALUE); + long lastModified = extractedFile.lastModified(); + if ((expectedModTime != lastModified) + || (expectedCrc != extractedFile.crc)) { + throw new IOException("Invalid extracted dex: " + extractedFile + + " (key \"" + prefsKeyPrefix + "\"), expected modification time: " + + expectedModTime + ", modification time: " + + lastModified + ", expected crc: " + + expectedCrc + ", file crc: " + extractedFile.crc); + } + files.add(extractedFile); + } else { + throw new IOException("Missing extracted secondary dex file '" + + extractedFile.getPath() + "'"); + } + } + + return files; + } + + + /** + * Compare current archive and crc with values stored in {@link SharedPreferences}. Should be + * called only while owning the lock on {@link #LOCK_FILENAME}. + */ + private static boolean isModified(Context context, File archive, long currentCrc, + String prefsKeyPrefix) { + SharedPreferences prefs = getMultiDexPreferences(context); + return (prefs.getLong(prefsKeyPrefix + KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive)) + || (prefs.getLong(prefsKeyPrefix + KEY_CRC, NO_VALUE) != currentCrc); + } + + private static long getTimeStamp(File archive) { + long timeStamp = archive.lastModified(); + if (timeStamp == NO_VALUE) { + // never return NO_VALUE + timeStamp--; + } + return timeStamp; + } + + + private static long getZipCrc(File archive) throws IOException { + long computedValue = ZipUtil.getZipCrc(archive); + if (computedValue == NO_VALUE) { + // never return NO_VALUE + computedValue--; + } + return computedValue; + } + + private List<ExtractedDex> performExtractions() throws IOException { + + final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; + + // It is safe to fully clear the dex dir because we own the file lock so no other process is + // extracting or running optimizing dexopt. It may cause crash of already running + // applications if for whatever reason we end up extracting again over a valid extraction. + clearDexDir(); + + List<ExtractedDex> files = new ArrayList<ExtractedDex>(); + + final ZipFile apk = new ZipFile(sourceApk); + try { + + int secondaryNumber = 2; + + ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); + while (dexFile != null) { + String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; + ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName); + files.add(extractedFile); + + Log.i(TAG, "Extraction is needed for file " + extractedFile); + int numAttempts = 0; + boolean isExtractionSuccessful = false; + while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { + numAttempts++; + + // Create a zip file (extractedFile) containing only the secondary dex file + // (dexFile) from the apk. + extract(apk, dexFile, extractedFile, extractedFilePrefix); + + // Read zip crc of extracted dex + try { + extractedFile.crc = getZipCrc(extractedFile); + isExtractionSuccessful = true; + } catch (IOException e) { + isExtractionSuccessful = false; + Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e); + } + + // Log size and crc of the extracted zip file + Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + + " '" + extractedFile.getAbsolutePath() + "': length " + + extractedFile.length() + " - crc: " + extractedFile.crc); + if (!isExtractionSuccessful) { + // Delete the extracted file + extractedFile.delete(); + if (extractedFile.exists()) { + Log.w(TAG, "Failed to delete corrupted secondary dex '" + + extractedFile.getPath() + "'"); + } + } + } + if (!isExtractionSuccessful) { + throw new IOException("Could not create zip file " + + extractedFile.getAbsolutePath() + " for secondary dex (" + + secondaryNumber + ")"); + } + secondaryNumber++; + dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); + } + } finally { + try { + apk.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close resource", e); + } + } + + return files; + } + + /** + * Save {@link SharedPreferences}. Should be called only while owning the lock on + * {@link #LOCK_FILENAME}. + */ + private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp, + long crc, List<ExtractedDex> extractedDexes) { + SharedPreferences prefs = getMultiDexPreferences(context); + SharedPreferences.Editor edit = prefs.edit(); + edit.putLong(keyPrefix + KEY_TIME_STAMP, timeStamp); + edit.putLong(keyPrefix + KEY_CRC, crc); + edit.putInt(keyPrefix + KEY_DEX_NUMBER, extractedDexes.size() + 1); + + int extractedDexId = 2; + for (ExtractedDex dex : extractedDexes) { + edit.putLong(keyPrefix + KEY_DEX_CRC + extractedDexId, dex.crc); + edit.putLong(keyPrefix + KEY_DEX_TIME + extractedDexId, dex.lastModified()); + extractedDexId++; + } + /* Use commit() and not apply() as advised by the doc because we need synchronous writing of + * the editor content and apply is doing an "asynchronous commit to disk". + */ + edit.commit(); + } + + /** + * Get the MuliDex {@link SharedPreferences} for the current application. Should be called only + * while owning the lock on {@link #LOCK_FILENAME}. + */ + private static SharedPreferences getMultiDexPreferences(Context context) { + return context.getSharedPreferences(PREFS_FILE, + Build.VERSION.SDK_INT < 11 /* Build.VERSION_CODES.HONEYCOMB */ + ? Context.MODE_PRIVATE + : Context.MODE_PRIVATE | 0x0004 /* Context.MODE_MULTI_PROCESS */); + } + + /** + * Clear the dex dir from all files but the lock. + */ + private void clearDexDir() { + File[] files = dexDir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return !pathname.getName().equals(LOCK_FILENAME); + } + }); + if (files == null) { + Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); + return; + } + for (File oldFile : files) { + Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + + oldFile.length()); + if (!oldFile.delete()) { + Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); + } else { + Log.i(TAG, "Deleted old file " + oldFile.getPath()); + } + } + } + + private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, + String extractedFilePrefix) throws IOException, FileNotFoundException { + + InputStream in = apk.getInputStream(dexFile); + ZipOutputStream out = null; + // Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir() + File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX, + extractTo.getParentFile()); + Log.i(TAG, "Extracting " + tmp.getPath()); + try { + out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); + try { + ZipEntry classesDex = new ZipEntry("classes.dex"); + // keep zip entry time since it is the criteria used by Dalvik + classesDex.setTime(dexFile.getTime()); + out.putNextEntry(classesDex); + + byte[] buffer = new byte[BUFFER_SIZE]; + int length = in.read(buffer); + while (length != -1) { + out.write(buffer, 0, length); + length = in.read(buffer); + } + out.closeEntry(); + } finally { + out.close(); + } + if (!tmp.setReadOnly()) { + throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() + + "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")"); + } + Log.i(TAG, "Renaming to " + extractTo.getPath()); + if (!tmp.renameTo(extractTo)) { + throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + + "\" to \"" + extractTo.getAbsolutePath() + "\""); + } + } finally { + closeQuietly(in); + tmp.delete(); // return status ignored + } + } + + /** + * Closes the given {@code Closeable}. Suppresses any IO exceptions. + */ + private static void closeQuietly(Closeable closeable) { + try { + closeable.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close resource", e); + } + } +} diff --git a/library/src/androidx/multidex/ZipUtil.java b/library/src/androidx/multidex/ZipUtil.java new file mode 100644 index 0000000..fc33623 --- /dev/null +++ b/library/src/androidx/multidex/ZipUtil.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Apache Harmony HEADER because the code in this class comes mostly from ZipFile, ZipEntry and + * ZipConstants from android libcore. + */ + +package androidx.multidex; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.zip.CRC32; +import java.util.zip.ZipException; + +/** + * Tools to build a quick partial crc of zip files. + */ +final class ZipUtil { + static class CentralDirectory { + long offset; + long size; + } + + /* redefine those constant here because of bug 13721174 preventing to compile using the + * constants defined in ZipFile */ + private static final int ENDHDR = 22; + private static final int ENDSIG = 0x6054b50; + + /** + * Size of reading buffers. + */ + private static final int BUFFER_SIZE = 0x4000; + + /** + * Compute crc32 of the central directory of an apk. The central directory contains + * the crc32 of each entries in the zip so the computed result is considered valid for the whole + * zip file. Does not support zip64 nor multidisk but it should be OK for now since ZipFile does + * not either. + */ + static long getZipCrc(File apk) throws IOException { + RandomAccessFile raf = new RandomAccessFile(apk, "r"); + try { + CentralDirectory dir = findCentralDirectory(raf); + + return computeCrcOfCentralDir(raf, dir); + } finally { + raf.close(); + } + } + + /* Package visible for testing */ + static CentralDirectory findCentralDirectory(RandomAccessFile raf) throws IOException, + ZipException { + long scanOffset = raf.length() - ENDHDR; + if (scanOffset < 0) { + throw new ZipException("File too short to be a zip file: " + raf.length()); + } + + long stopOffset = scanOffset - 0x10000 /* ".ZIP file comment"'s max length */; + if (stopOffset < 0) { + stopOffset = 0; + } + + int endSig = Integer.reverseBytes(ENDSIG); + while (true) { + raf.seek(scanOffset); + if (raf.readInt() == endSig) { + break; + } + + scanOffset--; + if (scanOffset < stopOffset) { + throw new ZipException("End Of Central Directory signature not found"); + } + } + // Read the End Of Central Directory. ENDHDR includes the signature + // bytes, + // which we've already read. + + // Pull out the information we need. + raf.skipBytes(2); // diskNumber + raf.skipBytes(2); // diskWithCentralDir + raf.skipBytes(2); // numEntries + raf.skipBytes(2); // totalNumEntries + CentralDirectory dir = new CentralDirectory(); + dir.size = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL; + dir.offset = Integer.reverseBytes(raf.readInt()) & 0xFFFFFFFFL; + return dir; + } + + /* Package visible for testing */ + static long computeCrcOfCentralDir(RandomAccessFile raf, CentralDirectory dir) + throws IOException { + CRC32 crc = new CRC32(); + long stillToRead = dir.size; + raf.seek(dir.offset); + int length = (int) Math.min(BUFFER_SIZE, stillToRead); + byte[] buffer = new byte[BUFFER_SIZE]; + length = raf.read(buffer, 0, length); + while (length != -1) { + crc.update(buffer, 0, length); + stillToRead -= length; + if (stillToRead == 0) { + break; + } + length = (int) Math.min(BUFFER_SIZE, stillToRead); + length = raf.read(buffer, 0, length); + } + return crc.getValue(); + } +} |