MultiDexExtractor.java revision 667f9a8a8155c41970a83be1414b57b5e37de336
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
17package android.support.multidex;
18
19import android.content.Context;
20import android.content.pm.ApplicationInfo;
21import android.util.Log;
22
23import java.io.Closeable;
24import java.io.File;
25import java.io.FileNotFoundException;
26import java.io.FileOutputStream;
27import java.io.FilenameFilter;
28import java.io.IOException;
29import java.io.InputStream;
30import java.util.ArrayList;
31import java.util.List;
32import java.util.zip.ZipEntry;
33import java.util.zip.ZipFile;
34import java.util.zip.ZipOutputStream;
35
36/**
37 * Exposes application secondary dex files as files in the application data
38 * directory.
39 */
40final class MultiDexExtractor {
41
42    private static final String TAG = MultiDex.TAG;
43
44    /**
45     * We look for additional dex files named {@code classes2.dex},
46     * {@code classes3.dex}, etc.
47     */
48    private static final String DEX_PREFIX = "classes";
49    private static final String DEX_SUFFIX = ".dex";
50
51    private static final String EXTRACTED_NAME_EXT = ".classes";
52    private static final String EXTRACTED_SUFFIX = ".zip";
53
54    private static final int BUFFER_SIZE = 0x4000;
55
56    /**
57     * Extracts application secondary dexes into files in the application data
58     * directory.
59     *
60     * @param dexDir
61     *
62     * @return a list of files that were created. The list may be empty if there
63     *         are no secondary dex files.
64     * @throws IOException if encounters a problem while reading or writing
65     *         secondary dex files
66     */
67    static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir)
68            throws IOException {
69
70        String extractedFilePrefix = new File(applicationInfo.sourceDir).getName()
71                + EXTRACTED_NAME_EXT;
72
73        prepareDexDir(dexDir, extractedFilePrefix);
74
75        final List<File> files = new ArrayList<File>();
76        ZipFile apk = new ZipFile(applicationInfo.sourceDir);
77        try {
78
79            int secondaryNumber = 2;
80
81            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
82            while (dexFile != null) {
83                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
84                File extractedFile = new File(dexDir, fileName);
85                files.add(extractedFile);
86
87                if (!extractedFile.isFile()) {
88                    extract(context, apk, dexFile, extractedFile, extractedFilePrefix);
89                }
90                secondaryNumber++;
91                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
92            }
93        } finally {
94            try {
95                apk.close();
96            } catch (IOException e) {
97                Log.w(TAG, "Failed to close resource", e);
98            }
99        }
100
101        return files;
102    }
103
104    private static void prepareDexDir(File dexDir, final String extractedFilePrefix)
105            throws IOException {
106        dexDir.mkdir();
107        if (!dexDir.isDirectory()) {
108            throw new IOException("Failed to create dex directory " + dexDir.getPath());
109        }
110
111        // Clean possible old files
112        FilenameFilter filter = new FilenameFilter() {
113            @Override
114            public boolean accept(File dir, String name) {
115                return !name.startsWith(extractedFilePrefix);
116            }
117        };
118        File[] files = dexDir.listFiles(filter);
119        if (files == null) {
120            Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
121            return;
122        }
123        for (File oldFile : files) {
124            if (!oldFile.delete()) {
125                Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
126            }
127        }
128    }
129
130    private static void extract(
131            Context context, ZipFile apk, ZipEntry dexFile, File extractTo,
132            String extractedFilePrefix) throws IOException, FileNotFoundException {
133
134        InputStream in = apk.getInputStream(dexFile);
135        ZipOutputStream out = null;
136        File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
137                extractTo.getParentFile());
138        Log.i(TAG, "Extracting " + tmp.getPath());
139        try {
140            out = new ZipOutputStream(new FileOutputStream(tmp));
141            try {
142                ZipEntry classesDex = new ZipEntry("classes.dex");
143                out.putNextEntry(classesDex);
144
145                byte[] buffer = new byte[BUFFER_SIZE];
146                int length = in.read(buffer);
147                while (length > 0) {
148                    out.write(buffer, 0, length);
149                    length = in.read(buffer);
150                }
151            } finally {
152                closeQuietly(out);
153            }
154            Log.i(TAG, "Renaming to " + extractTo.getPath());
155            if (!tmp.renameTo(extractTo)) {
156                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" +
157                        extractTo.getAbsolutePath() + "\"");
158            }
159        } finally {
160            closeQuietly(in);
161            tmp.delete(); // return status ignored
162        }
163    }
164
165    /**
166     * Closes the given {@code Closeable}. Suppresses any IO exceptions.
167     */
168    private static void closeQuietly(Closeable closeable) {
169        try {
170            closeable.close();
171        } catch (IOException e) {
172            Log.w(TAG, "Failed to close resource", e);
173        }
174    }
175}
176