blob: 2b961138da42552bbd5383e0241cc49c92d8f101 [file] [log] [blame]
Maurice Chu667f9a82013-10-16 13:12:22 -07001/*
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 Viverette475db102018-03-08 15:27:11 -050017package androidx.multidex;
Maurice Chu667f9a82013-10-16 13:12:22 -070018
Maurice Chu7e267a32014-01-15 19:02:18 -080019import android.content.Context;
20import android.content.SharedPreferences;
Maurice Chu994fa842014-01-17 10:42:54 -080021import android.os.Build;
Maurice Chu1f8c3492013-11-20 17:14:31 -080022import android.util.Log;
Maurice Chua159fd52013-11-28 12:59:37 -080023import java.io.BufferedOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070024import java.io.Closeable;
25import java.io.File;
Yohann Rousseld9eda552013-11-12 18:13:55 +010026import java.io.FileFilter;
Maurice Chu667f9a82013-10-16 13:12:22 -070027import java.io.FileNotFoundException;
28import java.io.FileOutputStream;
Maurice Chu667f9a82013-10-16 13:12:22 -070029import java.io.IOException;
30import java.io.InputStream;
Andrew Johnson048fbf72016-03-09 10:26:37 -050031import java.io.RandomAccessFile;
32import java.nio.channels.FileChannel;
33import java.nio.channels.FileLock;
Maurice Chu667f9a82013-10-16 13:12:22 -070034import java.util.ArrayList;
35import java.util.List;
36import java.util.zip.ZipEntry;
37import java.util.zip.ZipFile;
38import java.util.zip.ZipOutputStream;
39
40/**
41 * Exposes application secondary dex files as files in the application data
42 * directory.
Yohann Roussel08133852018-01-15 11:44:07 +010043 * {@link MultiDexExtractor} is taking the file lock in the dex dir on creation and release it
44 * during close.
Maurice Chu667f9a82013-10-16 13:12:22 -070045 */
Yohann Roussel08133852018-01-15 11:44:07 +010046final class MultiDexExtractor implements Closeable {
Maurice Chu667f9a82013-10-16 13:12:22 -070047
Yohann Roussel99581452017-01-02 17:57:27 +010048 /**
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 Chu667f9a82013-10-16 13:12:22 -070059 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 Rousselb2483812018-01-17 14:50:03 +010066 static final String DEX_SUFFIX = ".dex";
Maurice Chu667f9a82013-10-16 13:12:22 -070067
68 private static final String EXTRACTED_NAME_EXT = ".classes";
Yohann Rousselb2483812018-01-17 14:50:03 +010069 static final String EXTRACTED_SUFFIX = ".zip";
Maurice Chuf6d1f232013-11-27 12:56:14 -080070 private static final int MAX_EXTRACT_ATTEMPTS = 3;
Maurice Chu667f9a82013-10-16 13:12:22 -070071
Maurice Chu7e267a32014-01-15 19:02:18 -080072 private static final String PREFS_FILE = "multidex.version";
Yohann Roussel602c6ca2014-03-28 17:35:02 +010073 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 Roussel99581452017-01-02 17:57:27 +010076 private static final String KEY_DEX_CRC = "dex.crc.";
77 private static final String KEY_DEX_TIME = "dex.time.";
Yohann Roussel602c6ca2014-03-28 17:35:02 +010078
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 Chu7e267a32014-01-15 19:02:18 -080085
Andrew Johnson048fbf72016-03-09 10:26:37 -050086 private static final String LOCK_FILENAME = "MultiDex.lock";
Yohann Roussel08133852018-01-15 11:44:07 +010087 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 Johnson048fbf72016-03-09 10:26:37 -0500116
Maurice Chu667f9a82013-10-16 13:12:22 -0700117 /**
118 * Extracts application secondary dexes into files in the application data
119 * directory.
120 *
Maurice Chu667f9a82013-10-16 13:12:22 -0700121 * @return a list of files that were created. The list may be empty if there
Andrew Johnson048fbf72016-03-09 10:26:37 -0500122 * are no secondary dex files. Never return null.
Maurice Chu667f9a82013-10-16 13:12:22 -0700123 * @throws IOException if encounters a problem while reading or writing
124 * secondary dex files
125 */
Yohann Roussel08133852018-01-15 11:44:07 +0100126 List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
127 throws IOException {
Yohann Roussel50823412016-09-20 18:01:13 +0200128 Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
129 prefsKeyPrefix + ")");
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100130
Yohann Roussel08133852018-01-15 11:44:07 +0100131 if (!cacheLock.isValid()) {
132 throw new IllegalStateException("MultiDexExtractor was closed");
Andrew Johnson048fbf72016-03-09 10:26:37 -0500133 }
134
Yohann Roussel08133852018-01-15 11:44:07 +0100135 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 Roussel602c6ca2014-03-28 17:35:02 +0100155 }
156
157 Log.i(TAG, "load found " + files.size() + " secondary dex files");
158 return files;
159 }
160
Yohann Roussel08133852018-01-15 11:44:07 +0100161 @Override
162 public void close() throws IOException {
163 cacheLock.release();
164 lockChannel.close();
165 lockRaf.close();
166 }
167
Andrew Johnson048fbf72016-03-09 10:26:37 -0500168 /**
169 * Load previously extracted secondary dex files. Should be called only while owning the lock on
170 * {@link #LOCK_FILENAME}.
171 */
Yohann Roussel08133852018-01-15 11:44:07 +0100172 private List<ExtractedDex> loadExistingExtractions(
173 Context context,
Yohann Roussel50823412016-09-20 18:01:13 +0200174 String prefsKeyPrefix)
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100175 throws IOException {
176 Log.i(TAG, "loading existing secondary dex files");
177
178 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
Yohann Roussel99581452017-01-02 17:57:27 +0100179 SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
Yohann Roussel50823412016-09-20 18:01:13 +0200180 int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1);
Yohann Roussel99581452017-01-02 17:57:27 +0100181 final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100182
183 for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
184 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
Yohann Roussel99581452017-01-02 17:57:27 +0100185 ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100186 if (extractedFile.isFile()) {
Yohann Roussel99581452017-01-02 17:57:27 +0100187 extractedFile.crc = getZipCrc(extractedFile);
Yohann Roussel50823412016-09-20 18:01:13 +0200188 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 Roussel99581452017-01-02 17:57:27 +0100192 long lastModified = extractedFile.lastModified();
193 if ((expectedModTime != lastModified)
194 || (expectedCrc != extractedFile.crc)) {
195 throw new IOException("Invalid extracted dex: " + extractedFile +
Yohann Roussel50823412016-09-20 18:01:13 +0200196 " (key \"" + prefsKeyPrefix + "\"), expected modification time: "
Yohann Roussel99581452017-01-02 17:57:27 +0100197 + expectedModTime + ", modification time: "
198 + lastModified + ", expected crc: "
199 + expectedCrc + ", file crc: " + extractedFile.crc);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100200 }
Yohann Roussel99581452017-01-02 17:57:27 +0100201 files.add(extractedFile);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100202 } else {
203 throw new IOException("Missing extracted secondary dex file '" +
204 extractedFile.getPath() + "'");
205 }
206 }
207
208 return files;
209 }
210
Andrew Johnson048fbf72016-03-09 10:26:37 -0500211
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 Roussel50823412016-09-20 18:01:13 +0200216 private static boolean isModified(Context context, File archive, long currentCrc,
217 String prefsKeyPrefix) {
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100218 SharedPreferences prefs = getMultiDexPreferences(context);
Yohann Roussel50823412016-09-20 18:01:13 +0200219 return (prefs.getLong(prefsKeyPrefix + KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
220 || (prefs.getLong(prefsKeyPrefix + KEY_CRC, NO_VALUE) != currentCrc);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100221 }
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 Roussel08133852018-01-15 11:44:07 +0100242 private List<ExtractedDex> performExtractions() throws IOException {
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100243
Maurice Chu7e267a32014-01-15 19:02:18 -0800244 final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
Maurice Chu667f9a82013-10-16 13:12:22 -0700245
Yohann Roussel08133852018-01-15 11:44:07 +0100246 // 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 Chu667f9a82013-10-16 13:12:22 -0700250
Yohann Roussel99581452017-01-02 17:57:27 +0100251 List<ExtractedDex> files = new ArrayList<ExtractedDex>();
Maurice Chu7e267a32014-01-15 19:02:18 -0800252
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100253 final ZipFile apk = new ZipFile(sourceApk);
Maurice Chu667f9a82013-10-16 13:12:22 -0700254 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 Roussel99581452017-01-02 17:57:27 +0100261 ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
Maurice Chu667f9a82013-10-16 13:12:22 -0700262 files.add(extractedFile);
263
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100264 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 Chuf6d1f232013-11-27 12:56:14 -0800269
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100270 // Create a zip file (extractedFile) containing only the secondary dex file
271 // (dexFile) from the apk.
272 extract(apk, dexFile, extractedFile, extractedFilePrefix);
Maurice Chuf6d1f232013-11-27 12:56:14 -0800273
Yohann Roussel99581452017-01-02 17:57:27 +0100274 // 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 Chuf6d1f232013-11-27 12:56:14 -0800282
Yohann Roussel99581452017-01-02 17:57:27 +0100283 // Log size and crc of the extracted zip file
Yohann Roussel08133852018-01-15 11:44:07 +0100284 Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
285 + " '" + extractedFile.getAbsolutePath() + "': length "
286 + extractedFile.length() + " - crc: " + extractedFile.crc);
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100287 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 Chuf6d1f232013-11-27 12:56:14 -0800293 }
294 }
Yohann Roussel602c6ca2014-03-28 17:35:02 +0100295 }
296 if (!isExtractionSuccessful) {
297 throw new IOException("Could not create zip file " +
298 extractedFile.getAbsolutePath() + " for secondary dex (" +
299 secondaryNumber + ")");
Maurice Chu667f9a82013-10-16 13:12:22 -0700300 }
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 Johnson048fbf72016-03-09 10:26:37 -0500315 /**
316 * Save {@link SharedPreferences}. Should be called only while owning the lock on
317 * {@link #LOCK_FILENAME}.
318 */
Yohann Roussel50823412016-09-20 18:01:13 +0200319 private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp,
320 long crc, List<ExtractedDex> extractedDexes) {
Maurice Chu994fa842014-01-17 10:42:54 -0800321 SharedPreferences prefs = getMultiDexPreferences(context);
Maurice Chu7e267a32014-01-15 19:02:18 -0800322 SharedPreferences.Editor edit = prefs.edit();
Yohann Roussel50823412016-09-20 18:01:13 +0200323 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 Roussel99581452017-01-02 17:57:27 +0100326
327 int extractedDexId = 2;
328 for (ExtractedDex dex : extractedDexes) {
Yohann Roussel50823412016-09-20 18:01:13 +0200329 edit.putLong(keyPrefix + KEY_DEX_CRC + extractedDexId, dex.crc);
330 edit.putLong(keyPrefix + KEY_DEX_TIME + extractedDexId, dex.lastModified());
Yohann Roussel99581452017-01-02 17:57:27 +0100331 extractedDexId++;
332 }
Andrew Johnson048fbf72016-03-09 10:26:37 -0500333 /* 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 Chu7e267a32014-01-15 19:02:18 -0800337 }
338
Andrew Johnson048fbf72016-03-09 10:26:37 -0500339 /**
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 Chu994fa842014-01-17 10:42:54 -0800343 private static SharedPreferences getMultiDexPreferences(Context context) {
344 return context.getSharedPreferences(PREFS_FILE,
Yohann Roussele99daea2014-09-17 16:49:21 +0200345 Build.VERSION.SDK_INT < 11 /* Build.VERSION_CODES.HONEYCOMB */
Maurice Chu994fa842014-01-17 10:42:54 -0800346 ? Context.MODE_PRIVATE
Yohann Roussele99daea2014-09-17 16:49:21 +0200347 : Context.MODE_PRIVATE | 0x0004 /* Context.MODE_MULTI_PROCESS */);
Maurice Chu994fa842014-01-17 10:42:54 -0800348 }
349
Maurice Chu7e267a32014-01-15 19:02:18 -0800350 /**
Yohann Roussel08133852018-01-15 11:44:07 +0100351 * Clear the dex dir from all files but the lock.
Maurice Chucc63eda2013-12-02 15:39:59 -0800352 */
Yohann Roussel08133852018-01-15 11:44:07 +0100353 private void clearDexDir() {
354 File[] files = dexDir.listFiles(new FileFilter() {
Maurice Chu667f9a82013-10-16 13:12:22 -0700355 @Override
Yohann Rousseld9eda552013-11-12 18:13:55 +0100356 public boolean accept(File pathname) {
Yohann Roussel08133852018-01-15 11:44:07 +0100357 return !pathname.getName().equals(LOCK_FILENAME);
Maurice Chu667f9a82013-10-16 13:12:22 -0700358 }
Yohann Roussel08133852018-01-15 11:44:07 +0100359 });
Maurice Chu667f9a82013-10-16 13:12:22 -0700360 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 Rousseld79604b2014-07-08 16:50:10 +0200365 Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " +
Maurice Chu7e267a32014-01-15 19:02:18 -0800366 oldFile.length());
Maurice Chu667f9a82013-10-16 13:12:22 -0700367 if (!oldFile.delete()) {
368 Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
Yohann Roussel88117c32013-11-28 23:22:11 +0100369 } else {
Yohann Rousseld79604b2014-07-08 16:50:10 +0200370 Log.i(TAG, "Deleted old file " + oldFile.getPath());
Maurice Chu667f9a82013-10-16 13:12:22 -0700371 }
372 }
373 }
374
Yohann Roussel52eafa02013-11-21 11:46:53 +0100375 private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
Maurice Chu7e267a32014-01-15 19:02:18 -0800376 String extractedFilePrefix) throws IOException, FileNotFoundException {
Maurice Chu667f9a82013-10-16 13:12:22 -0700377
378 InputStream in = apk.getInputStream(dexFile);
379 ZipOutputStream out = null;
Jon Noack4b56ee22017-01-12 08:55:14 -0600380 // Temp files must not start with extractedFilePrefix to get cleaned up in prepareDexDir()
381 File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,
Maurice Chu667f9a82013-10-16 13:12:22 -0700382 extractTo.getParentFile());
383 Log.i(TAG, "Extracting " + tmp.getPath());
384 try {
Maurice Chua159fd52013-11-28 12:59:37 -0800385 out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
Maurice Chu667f9a82013-10-16 13:12:22 -0700386 try {
387 ZipEntry classesDex = new ZipEntry("classes.dex");
Yohann Rousseledf07172013-11-12 18:11:02 +0100388 // keep zip entry time since it is the criteria used by Dalvik
389 classesDex.setTime(dexFile.getTime());
Maurice Chu667f9a82013-10-16 13:12:22 -0700390 out.putNextEntry(classesDex);
391
392 byte[] buffer = new byte[BUFFER_SIZE];
393 int length = in.read(buffer);
Maurice Chu1f8c3492013-11-20 17:14:31 -0800394 while (length != -1) {
Yohann Roussel52eafa02013-11-21 11:46:53 +0100395 out.write(buffer, 0, length);
Maurice Chu667f9a82013-10-16 13:12:22 -0700396 length = in.read(buffer);
397 }
Maurice Chua159fd52013-11-28 12:59:37 -0800398 out.closeEntry();
Maurice Chu667f9a82013-10-16 13:12:22 -0700399 } finally {
Maurice Chua0c1a852013-11-27 19:01:53 -0800400 out.close();
Maurice Chu667f9a82013-10-16 13:12:22 -0700401 }
Yohann Rousself5832472017-01-02 15:29:59 +0100402 if (!tmp.setReadOnly()) {
403 throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +
404 "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
405 }
Maurice Chu667f9a82013-10-16 13:12:22 -0700406 Log.i(TAG, "Renaming to " + extractTo.getPath());
Maurice Chua159fd52013-11-28 12:59:37 -0800407 if (!tmp.renameTo(extractTo)) {
408 throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
409 "\" to \"" + extractTo.getAbsolutePath() + "\"");
Maurice Chu667f9a82013-10-16 13:12:22 -0700410 }
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 Chu667f9a82013-10-16 13:12:22 -0700427}