DataExporter.java revision a6ec38053a00fb399ca174931c149e3740c7420a
1/*
2 * Copyright (C) 2012 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 com.android.providers.contacts.debug;
18
19import com.android.providers.contacts.util.Hex;
20import com.google.common.io.Closeables;
21
22import android.content.Context;
23import android.net.Uri;
24import android.util.Log;
25
26import java.io.File;
27import java.io.FileInputStream;
28import java.io.FileOutputStream;
29import java.io.IOException;
30import java.io.InputStream;
31import java.security.SecureRandom;
32import java.util.zip.Deflater;
33import java.util.zip.ZipEntry;
34import java.util.zip.ZipOutputStream;
35
36/**
37 * Compress all files under the app data dir into a single zip file.
38 *
39 * Make sure not to output dump filenames anywhere, including logcat.
40 */
41public class DataExporter {
42    private static String TAG = "DataExporter";
43
44    public static final String ZIP_MIME_TYPE = "application/zip";
45
46    public static final String DUMP_FILE_DIRECTORY_NAME = "dumpedfiles";
47
48    public static final String OUT_FILE_SUFFIX = "-contacts-db.zip";
49
50    /**
51     * Compress all files under the app data dir into a single zip file, and return the content://
52     * URI to the file, which can be read via {@link DumpFileProvider}.
53     */
54    public static Uri exportData(Context context) throws IOException {
55        final String fileName = generateRandomName() + OUT_FILE_SUFFIX;
56        final File outFile = getOutputFile(context, fileName);
57
58        // Remove all existing ones.
59        removeDumpFiles(context);
60
61        Log.i(TAG, "Dump started...");
62
63        ensureOutputDirectory(context);
64        final ZipOutputStream os = new ZipOutputStream(new FileOutputStream(outFile));
65        os.setLevel(Deflater.BEST_COMPRESSION);
66        try {
67            addDirectory(context, os, context.getFilesDir().getParentFile(), "contacts-files");
68        } finally {
69            Closeables.closeQuietly(os);
70        }
71        Log.i(TAG, "Dump finished.");
72        return DumpFileProvider.AUTHORITY_URI.buildUpon().appendPath(fileName).build();
73    }
74
75    /** @return long random string for a file name */
76    private static String generateRandomName() {
77        final SecureRandom rng = new SecureRandom();
78        final byte[] random = new byte[256 / 8];
79        rng.nextBytes(random);
80
81        return Hex.encodeHex(random, true);
82    }
83
84    private static File getOutputDirectory(Context context) {
85        return new File(context.getCacheDir(), DUMP_FILE_DIRECTORY_NAME);
86    }
87
88    private static void ensureOutputDirectory(Context context) {
89        final File directory = getOutputDirectory(context);
90        if (!directory.exists()) {
91            directory.mkdir();
92        }
93    }
94
95    public static File getOutputFile(Context context, String fileName) {
96        return new File(getOutputDirectory(context), fileName);
97    }
98
99    public static boolean dumpFileExists(Context context) {
100        return getOutputDirectory(context).exists();
101    }
102
103    public static void removeDumpFiles(Context context) {
104        removeFileOrDirectory(getOutputDirectory(context));
105    }
106
107    private static void removeFileOrDirectory(File file) {
108        if (!file.exists()) return;
109
110        if (file.isFile()) {
111            Log.i(TAG, "Removing " + file);
112            file.delete();
113            return;
114        }
115
116        if (file.isDirectory()) {
117            for (File child : file.listFiles()) {
118                removeFileOrDirectory(child);
119            }
120            Log.i(TAG, "Removing " + file);
121            file.delete();
122        }
123    }
124
125    /**
126     * Add all files under {@code current} to {@code os} zip stream
127     */
128    private static void addDirectory(Context context, ZipOutputStream os, File current,
129            String storedPath) throws IOException {
130        for (File child : current.listFiles()) {
131            final String childStoredPath = storedPath + "/" + child.getName();
132
133            if (child.isDirectory()) {
134                // Don't need the cache directory, which also contains the dump files.
135                if (child.equals(context.getCacheDir())) {
136                    continue;
137                }
138                // This check is redundant as the output directory should be in the cache dir,
139                // but just in case...
140                if (child.getName().equals(DUMP_FILE_DIRECTORY_NAME)) {
141                    continue;
142                }
143                addDirectory(context, os, child, childStoredPath);
144            } else if (child.isFile()) {
145                addFile(os, child, childStoredPath);
146            } else {
147                // Shouldn't happen; skip.
148            }
149        }
150    }
151
152    /**
153     * Add a single file {@code current} to {@code os} zip stream using the file name
154     * {@code storedPath}.
155     */
156    private static void addFile(ZipOutputStream os, File current, String storedPath)
157            throws IOException {
158        Log.i(TAG, "Adding " + current.getAbsolutePath() + " ...");
159        final InputStream is = new FileInputStream(current);
160        os.putNextEntry(new ZipEntry(storedPath));
161
162        final byte[] buf = new byte[32 * 1024];
163        int totalLen = 0;
164        while (true) {
165            int len = is.read(buf);
166            if (len <= 0) {
167                break;
168            }
169            os.write(buf, 0, len);
170            totalLen += len;
171        }
172        os.closeEntry();
173        Log.i(TAG, "Added " + current.getAbsolutePath() + " as " + storedPath +
174                " (" + totalLen + " bytes)");
175    }
176}
177