Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2013 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
Alan Viverette | 475db10 | 2018-03-08 15:27:11 -0500 | [diff] [blame^] | 17 | package androidx.multidex; |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 18 | |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 19 | import android.content.Context; |
| 20 | import android.content.SharedPreferences; |
Maurice Chu | 994fa84 | 2014-01-17 10:42:54 -0800 | [diff] [blame] | 21 | import android.os.Build; |
Maurice Chu | 1f8c349 | 2013-11-20 17:14:31 -0800 | [diff] [blame] | 22 | import android.util.Log; |
Maurice Chu | a159fd5 | 2013-11-28 12:59:37 -0800 | [diff] [blame] | 23 | import java.io.BufferedOutputStream; |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 24 | import java.io.Closeable; |
| 25 | import java.io.File; |
Yohann Roussel | d9eda55 | 2013-11-12 18:13:55 +0100 | [diff] [blame] | 26 | import java.io.FileFilter; |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 27 | import java.io.FileNotFoundException; |
| 28 | import java.io.FileOutputStream; |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 29 | import java.io.IOException; |
| 30 | import java.io.InputStream; |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 31 | import java.io.RandomAccessFile; |
| 32 | import java.nio.channels.FileChannel; |
| 33 | import java.nio.channels.FileLock; |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 34 | import java.util.ArrayList; |
| 35 | import java.util.List; |
| 36 | import java.util.zip.ZipEntry; |
| 37 | import java.util.zip.ZipFile; |
| 38 | import java.util.zip.ZipOutputStream; |
| 39 | |
| 40 | /** |
| 41 | * Exposes application secondary dex files as files in the application data |
| 42 | * directory. |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 43 | * {@link MultiDexExtractor} is taking the file lock in the dex dir on creation and release it |
| 44 | * during close. |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 45 | */ |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 46 | final class MultiDexExtractor implements Closeable { |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 47 | |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 48 | /** |
| 49 | * Zip file containing one secondary dex file. |
| 50 | */ |
| 51 | private static class ExtractedDex extends File { |
| 52 | public long crc = NO_VALUE; |
| 53 | |
| 54 | public ExtractedDex(File dexDir, String fileName) { |
| 55 | super(dexDir, fileName); |
| 56 | } |
| 57 | } |
| 58 | |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 59 | private static final String TAG = MultiDex.TAG; |
| 60 | |
| 61 | /** |
| 62 | * We look for additional dex files named {@code classes2.dex}, |
| 63 | * {@code classes3.dex}, etc. |
| 64 | */ |
| 65 | private static final String DEX_PREFIX = "classes"; |
Yohann Roussel | b248381 | 2018-01-17 14:50:03 +0100 | [diff] [blame] | 66 | static final String DEX_SUFFIX = ".dex"; |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 67 | |
| 68 | private static final String EXTRACTED_NAME_EXT = ".classes"; |
Yohann Roussel | b248381 | 2018-01-17 14:50:03 +0100 | [diff] [blame] | 69 | static final String EXTRACTED_SUFFIX = ".zip"; |
Maurice Chu | f6d1f23 | 2013-11-27 12:56:14 -0800 | [diff] [blame] | 70 | private static final int MAX_EXTRACT_ATTEMPTS = 3; |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 71 | |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 72 | private static final String PREFS_FILE = "multidex.version"; |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 73 | private static final String KEY_TIME_STAMP = "timestamp"; |
| 74 | private static final String KEY_CRC = "crc"; |
| 75 | private static final String KEY_DEX_NUMBER = "dex.number"; |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 76 | private static final String KEY_DEX_CRC = "dex.crc."; |
| 77 | private static final String KEY_DEX_TIME = "dex.time."; |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 78 | |
| 79 | /** |
| 80 | * Size of reading buffers. |
| 81 | */ |
| 82 | private static final int BUFFER_SIZE = 0x4000; |
| 83 | /* Keep value away from 0 because it is a too probable time stamp value */ |
| 84 | private static final long NO_VALUE = -1L; |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 85 | |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 86 | private static final String LOCK_FILENAME = "MultiDex.lock"; |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 87 | private final File sourceApk; |
| 88 | private final long sourceCrc; |
| 89 | private final File dexDir; |
| 90 | private final RandomAccessFile lockRaf; |
| 91 | private final FileChannel lockChannel; |
| 92 | private final FileLock cacheLock; |
| 93 | |
| 94 | MultiDexExtractor(File sourceApk, File dexDir) throws IOException { |
| 95 | Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")"); |
| 96 | this.sourceApk = sourceApk; |
| 97 | this.dexDir = dexDir; |
| 98 | sourceCrc = getZipCrc(sourceApk); |
| 99 | File lockFile = new File(dexDir, LOCK_FILENAME); |
| 100 | lockRaf = new RandomAccessFile(lockFile, "rw"); |
| 101 | try { |
| 102 | lockChannel = lockRaf.getChannel(); |
| 103 | try { |
| 104 | Log.i(TAG, "Blocking on lock " + lockFile.getPath()); |
| 105 | cacheLock = lockChannel.lock(); |
| 106 | } catch (IOException | RuntimeException | Error e) { |
| 107 | closeQuietly(lockChannel); |
| 108 | throw e; |
| 109 | } |
| 110 | Log.i(TAG, lockFile.getPath() + " locked"); |
| 111 | } catch (IOException | RuntimeException | Error e) { |
| 112 | closeQuietly(lockRaf); |
| 113 | throw e; |
| 114 | } |
| 115 | } |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 116 | |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 117 | /** |
| 118 | * Extracts application secondary dexes into files in the application data |
| 119 | * directory. |
| 120 | * |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 121 | * @return a list of files that were created. The list may be empty if there |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 122 | * are no secondary dex files. Never return null. |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 123 | * @throws IOException if encounters a problem while reading or writing |
| 124 | * secondary dex files |
| 125 | */ |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 126 | List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) |
| 127 | throws IOException { |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 128 | Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " + |
| 129 | prefsKeyPrefix + ")"); |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 130 | |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 131 | if (!cacheLock.isValid()) { |
| 132 | throw new IllegalStateException("MultiDexExtractor was closed"); |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 133 | } |
| 134 | |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 135 | List<ExtractedDex> files; |
| 136 | if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) { |
| 137 | try { |
| 138 | files = loadExistingExtractions(context, prefsKeyPrefix); |
| 139 | } catch (IOException ioe) { |
| 140 | Log.w(TAG, "Failed to reload existing extracted secondary dex files," |
| 141 | + " falling back to fresh extraction", ioe); |
| 142 | files = performExtractions(); |
| 143 | putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc, |
| 144 | files); |
| 145 | } |
| 146 | } else { |
| 147 | if (forceReload) { |
| 148 | Log.i(TAG, "Forced extraction must be performed."); |
| 149 | } else { |
| 150 | Log.i(TAG, "Detected that extraction must be performed."); |
| 151 | } |
| 152 | files = performExtractions(); |
| 153 | putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc, |
| 154 | files); |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 155 | } |
| 156 | |
| 157 | Log.i(TAG, "load found " + files.size() + " secondary dex files"); |
| 158 | return files; |
| 159 | } |
| 160 | |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 161 | @Override |
| 162 | public void close() throws IOException { |
| 163 | cacheLock.release(); |
| 164 | lockChannel.close(); |
| 165 | lockRaf.close(); |
| 166 | } |
| 167 | |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 168 | /** |
| 169 | * Load previously extracted secondary dex files. Should be called only while owning the lock on |
| 170 | * {@link #LOCK_FILENAME}. |
| 171 | */ |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 172 | private List<ExtractedDex> loadExistingExtractions( |
| 173 | Context context, |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 174 | String prefsKeyPrefix) |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 175 | throws IOException { |
| 176 | Log.i(TAG, "loading existing secondary dex files"); |
| 177 | |
| 178 | final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 179 | SharedPreferences multiDexPreferences = getMultiDexPreferences(context); |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 180 | int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1); |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 181 | final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1); |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 182 | |
| 183 | for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) { |
| 184 | String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 185 | ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName); |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 186 | if (extractedFile.isFile()) { |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 187 | extractedFile.crc = getZipCrc(extractedFile); |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 188 | long expectedCrc = multiDexPreferences.getLong( |
| 189 | prefsKeyPrefix + KEY_DEX_CRC + secondaryNumber, NO_VALUE); |
| 190 | long expectedModTime = multiDexPreferences.getLong( |
| 191 | prefsKeyPrefix + KEY_DEX_TIME + secondaryNumber, NO_VALUE); |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 192 | long lastModified = extractedFile.lastModified(); |
| 193 | if ((expectedModTime != lastModified) |
| 194 | || (expectedCrc != extractedFile.crc)) { |
| 195 | throw new IOException("Invalid extracted dex: " + extractedFile + |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 196 | " (key \"" + prefsKeyPrefix + "\"), expected modification time: " |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 197 | + expectedModTime + ", modification time: " |
| 198 | + lastModified + ", expected crc: " |
| 199 | + expectedCrc + ", file crc: " + extractedFile.crc); |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 200 | } |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 201 | files.add(extractedFile); |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 202 | } else { |
| 203 | throw new IOException("Missing extracted secondary dex file '" + |
| 204 | extractedFile.getPath() + "'"); |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | return files; |
| 209 | } |
| 210 | |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 211 | |
| 212 | /** |
| 213 | * Compare current archive and crc with values stored in {@link SharedPreferences}. Should be |
| 214 | * called only while owning the lock on {@link #LOCK_FILENAME}. |
| 215 | */ |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 216 | private static boolean isModified(Context context, File archive, long currentCrc, |
| 217 | String prefsKeyPrefix) { |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 218 | SharedPreferences prefs = getMultiDexPreferences(context); |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 219 | return (prefs.getLong(prefsKeyPrefix + KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive)) |
| 220 | || (prefs.getLong(prefsKeyPrefix + KEY_CRC, NO_VALUE) != currentCrc); |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 221 | } |
| 222 | |
| 223 | private static long getTimeStamp(File archive) { |
| 224 | long timeStamp = archive.lastModified(); |
| 225 | if (timeStamp == NO_VALUE) { |
| 226 | // never return NO_VALUE |
| 227 | timeStamp--; |
| 228 | } |
| 229 | return timeStamp; |
| 230 | } |
| 231 | |
| 232 | |
| 233 | private static long getZipCrc(File archive) throws IOException { |
| 234 | long computedValue = ZipUtil.getZipCrc(archive); |
| 235 | if (computedValue == NO_VALUE) { |
| 236 | // never return NO_VALUE |
| 237 | computedValue--; |
| 238 | } |
| 239 | return computedValue; |
| 240 | } |
| 241 | |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 242 | private List<ExtractedDex> performExtractions() throws IOException { |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 243 | |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 244 | final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 245 | |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 246 | // It is safe to fully clear the dex dir because we own the file lock so no other process is |
| 247 | // extracting or running optimizing dexopt. It may cause crash of already running |
| 248 | // applications if for whatever reason we end up extracting again over a valid extraction. |
| 249 | clearDexDir(); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 250 | |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 251 | List<ExtractedDex> files = new ArrayList<ExtractedDex>(); |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 252 | |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 253 | final ZipFile apk = new ZipFile(sourceApk); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 254 | try { |
| 255 | |
| 256 | int secondaryNumber = 2; |
| 257 | |
| 258 | ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); |
| 259 | while (dexFile != null) { |
| 260 | String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 261 | ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 262 | files.add(extractedFile); |
| 263 | |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 264 | Log.i(TAG, "Extraction is needed for file " + extractedFile); |
| 265 | int numAttempts = 0; |
| 266 | boolean isExtractionSuccessful = false; |
| 267 | while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { |
| 268 | numAttempts++; |
Maurice Chu | f6d1f23 | 2013-11-27 12:56:14 -0800 | [diff] [blame] | 269 | |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 270 | // Create a zip file (extractedFile) containing only the secondary dex file |
| 271 | // (dexFile) from the apk. |
| 272 | extract(apk, dexFile, extractedFile, extractedFilePrefix); |
Maurice Chu | f6d1f23 | 2013-11-27 12:56:14 -0800 | [diff] [blame] | 273 | |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 274 | // Read zip crc of extracted dex |
| 275 | try { |
| 276 | extractedFile.crc = getZipCrc(extractedFile); |
| 277 | isExtractionSuccessful = true; |
| 278 | } catch (IOException e) { |
| 279 | isExtractionSuccessful = false; |
| 280 | Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e); |
| 281 | } |
Maurice Chu | f6d1f23 | 2013-11-27 12:56:14 -0800 | [diff] [blame] | 282 | |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 283 | // Log size and crc of the extracted zip file |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 284 | Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") |
| 285 | + " '" + extractedFile.getAbsolutePath() + "': length " |
| 286 | + extractedFile.length() + " - crc: " + extractedFile.crc); |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 287 | if (!isExtractionSuccessful) { |
| 288 | // Delete the extracted file |
| 289 | extractedFile.delete(); |
| 290 | if (extractedFile.exists()) { |
| 291 | Log.w(TAG, "Failed to delete corrupted secondary dex '" + |
| 292 | extractedFile.getPath() + "'"); |
Maurice Chu | f6d1f23 | 2013-11-27 12:56:14 -0800 | [diff] [blame] | 293 | } |
| 294 | } |
Yohann Roussel | 602c6ca | 2014-03-28 17:35:02 +0100 | [diff] [blame] | 295 | } |
| 296 | if (!isExtractionSuccessful) { |
| 297 | throw new IOException("Could not create zip file " + |
| 298 | extractedFile.getAbsolutePath() + " for secondary dex (" + |
| 299 | secondaryNumber + ")"); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 300 | } |
| 301 | secondaryNumber++; |
| 302 | dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); |
| 303 | } |
| 304 | } finally { |
| 305 | try { |
| 306 | apk.close(); |
| 307 | } catch (IOException e) { |
| 308 | Log.w(TAG, "Failed to close resource", e); |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | return files; |
| 313 | } |
| 314 | |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 315 | /** |
| 316 | * Save {@link SharedPreferences}. Should be called only while owning the lock on |
| 317 | * {@link #LOCK_FILENAME}. |
| 318 | */ |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 319 | private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp, |
| 320 | long crc, List<ExtractedDex> extractedDexes) { |
Maurice Chu | 994fa84 | 2014-01-17 10:42:54 -0800 | [diff] [blame] | 321 | SharedPreferences prefs = getMultiDexPreferences(context); |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 322 | SharedPreferences.Editor edit = prefs.edit(); |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 323 | edit.putLong(keyPrefix + KEY_TIME_STAMP, timeStamp); |
| 324 | edit.putLong(keyPrefix + KEY_CRC, crc); |
| 325 | edit.putInt(keyPrefix + KEY_DEX_NUMBER, extractedDexes.size() + 1); |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 326 | |
| 327 | int extractedDexId = 2; |
| 328 | for (ExtractedDex dex : extractedDexes) { |
Yohann Roussel | 5082341 | 2016-09-20 18:01:13 +0200 | [diff] [blame] | 329 | edit.putLong(keyPrefix + KEY_DEX_CRC + extractedDexId, dex.crc); |
| 330 | edit.putLong(keyPrefix + KEY_DEX_TIME + extractedDexId, dex.lastModified()); |
Yohann Roussel | 9958145 | 2017-01-02 17:57:27 +0100 | [diff] [blame] | 331 | extractedDexId++; |
| 332 | } |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 333 | /* Use commit() and not apply() as advised by the doc because we need synchronous writing of |
| 334 | * the editor content and apply is doing an "asynchronous commit to disk". |
| 335 | */ |
| 336 | edit.commit(); |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 337 | } |
| 338 | |
Andrew Johnson | 048fbf7 | 2016-03-09 10:26:37 -0500 | [diff] [blame] | 339 | /** |
| 340 | * Get the MuliDex {@link SharedPreferences} for the current application. Should be called only |
| 341 | * while owning the lock on {@link #LOCK_FILENAME}. |
| 342 | */ |
Maurice Chu | 994fa84 | 2014-01-17 10:42:54 -0800 | [diff] [blame] | 343 | private static SharedPreferences getMultiDexPreferences(Context context) { |
| 344 | return context.getSharedPreferences(PREFS_FILE, |
Yohann Roussel | e99daea | 2014-09-17 16:49:21 +0200 | [diff] [blame] | 345 | Build.VERSION.SDK_INT < 11 /* Build.VERSION_CODES.HONEYCOMB */ |
Maurice Chu | 994fa84 | 2014-01-17 10:42:54 -0800 | [diff] [blame] | 346 | ? Context.MODE_PRIVATE |
Yohann Roussel | e99daea | 2014-09-17 16:49:21 +0200 | [diff] [blame] | 347 | : Context.MODE_PRIVATE | 0x0004 /* Context.MODE_MULTI_PROCESS */); |
Maurice Chu | 994fa84 | 2014-01-17 10:42:54 -0800 | [diff] [blame] | 348 | } |
| 349 | |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 350 | /** |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 351 | * Clear the dex dir from all files but the lock. |
Maurice Chu | cc63eda | 2013-12-02 15:39:59 -0800 | [diff] [blame] | 352 | */ |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 353 | private void clearDexDir() { |
| 354 | File[] files = dexDir.listFiles(new FileFilter() { |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 355 | @Override |
Yohann Roussel | d9eda55 | 2013-11-12 18:13:55 +0100 | [diff] [blame] | 356 | public boolean accept(File pathname) { |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 357 | return !pathname.getName().equals(LOCK_FILENAME); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 358 | } |
Yohann Roussel | 0813385 | 2018-01-15 11:44:07 +0100 | [diff] [blame] | 359 | }); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 360 | if (files == null) { |
| 361 | Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); |
| 362 | return; |
| 363 | } |
| 364 | for (File oldFile : files) { |
Yohann Roussel | d79604b | 2014-07-08 16:50:10 +0200 | [diff] [blame] | 365 | Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 366 | oldFile.length()); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 367 | if (!oldFile.delete()) { |
| 368 | Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); |
Yohann Roussel | 88117c3 | 2013-11-28 23:22:11 +0100 | [diff] [blame] | 369 | } else { |
Yohann Roussel | d79604b | 2014-07-08 16:50:10 +0200 | [diff] [blame] | 370 | Log.i(TAG, "Deleted old file " + oldFile.getPath()); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 371 | } |
| 372 | } |
| 373 | } |
| 374 | |
Yohann Roussel | 52eafa0 | 2013-11-21 11:46:53 +0100 | [diff] [blame] | 375 | private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, |
Maurice Chu | 7e267a3 | 2014-01-15 19:02:18 -0800 | [diff] [blame] | 376 | String extractedFilePrefix) throws IOException, FileNotFoundException { |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 377 | |
| 378 | InputStream in = apk.getInputStream(dexFile); |
| 379 | ZipOutputStream out = null; |
Jon Noack | 4b56ee2 | 2017-01-12 08:55:14 -0600 | [diff] [blame] | 380 | // Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir() |
| 381 | File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX, |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 382 | extractTo.getParentFile()); |
| 383 | Log.i(TAG, "Extracting " + tmp.getPath()); |
| 384 | try { |
Maurice Chu | a159fd5 | 2013-11-28 12:59:37 -0800 | [diff] [blame] | 385 | out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 386 | try { |
| 387 | ZipEntry classesDex = new ZipEntry("classes.dex"); |
Yohann Roussel | edf0717 | 2013-11-12 18:11:02 +0100 | [diff] [blame] | 388 | // keep zip entry time since it is the criteria used by Dalvik |
| 389 | classesDex.setTime(dexFile.getTime()); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 390 | out.putNextEntry(classesDex); |
| 391 | |
| 392 | byte[] buffer = new byte[BUFFER_SIZE]; |
| 393 | int length = in.read(buffer); |
Maurice Chu | 1f8c349 | 2013-11-20 17:14:31 -0800 | [diff] [blame] | 394 | while (length != -1) { |
Yohann Roussel | 52eafa0 | 2013-11-21 11:46:53 +0100 | [diff] [blame] | 395 | out.write(buffer, 0, length); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 396 | length = in.read(buffer); |
| 397 | } |
Maurice Chu | a159fd5 | 2013-11-28 12:59:37 -0800 | [diff] [blame] | 398 | out.closeEntry(); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 399 | } finally { |
Maurice Chu | a0c1a85 | 2013-11-27 19:01:53 -0800 | [diff] [blame] | 400 | out.close(); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 401 | } |
Yohann Roussel | f583247 | 2017-01-02 15:29:59 +0100 | [diff] [blame] | 402 | if (!tmp.setReadOnly()) { |
| 403 | throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() + |
| 404 | "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")"); |
| 405 | } |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 406 | Log.i(TAG, "Renaming to " + extractTo.getPath()); |
Maurice Chu | a159fd5 | 2013-11-28 12:59:37 -0800 | [diff] [blame] | 407 | if (!tmp.renameTo(extractTo)) { |
| 408 | throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + |
| 409 | "\" to \"" + extractTo.getAbsolutePath() + "\""); |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 410 | } |
| 411 | } finally { |
| 412 | closeQuietly(in); |
| 413 | tmp.delete(); // return status ignored |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | /** |
| 418 | * Closes the given {@code Closeable}. Suppresses any IO exceptions. |
| 419 | */ |
| 420 | private static void closeQuietly(Closeable closeable) { |
| 421 | try { |
| 422 | closeable.close(); |
| 423 | } catch (IOException e) { |
| 424 | Log.w(TAG, "Failed to close resource", e); |
| 425 | } |
| 426 | } |
Maurice Chu | 667f9a8 | 2013-10-16 13:12:22 -0700 | [diff] [blame] | 427 | } |