• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.providers.contacts.debug;
18 
19 import com.android.providers.contacts.util.Hex;
20 import com.google.common.io.Closeables;
21 
22 import android.content.Context;
23 import android.net.Uri;
24 import android.util.Log;
25 
26 import java.io.File;
27 import java.io.FileInputStream;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.security.SecureRandom;
32 import java.util.zip.Deflater;
33 import java.util.zip.ZipEntry;
34 import 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  */
41 public 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     public static final String VALID_FILE_NAME_REGEX = "[0-9A-Fa-f]+-contacts-db\\.zip";
50 
51     /**
52      * Compress all files under the app data dir into a single zip file, and return the content://
53      * URI to the file, which can be read via {@link DumpFileProvider}.
54      */
exportData(Context context)55     public static Uri exportData(Context context) throws IOException {
56         final String fileName = generateRandomName() + OUT_FILE_SUFFIX;
57         final File outFile = getOutputFile(context, fileName);
58 
59         // Remove all existing ones.
60         removeDumpFiles(context);
61 
62         Log.i(TAG, "Dump started...");
63 
64         ensureOutputDirectory(context);
65         final ZipOutputStream os = new ZipOutputStream(new FileOutputStream(outFile));
66         os.setLevel(Deflater.BEST_COMPRESSION);
67         try {
68             addDirectory(context, os, context.getFilesDir().getParentFile(), "contacts-files");
69         } finally {
70             Closeables.closeQuietly(os);
71         }
72         Log.i(TAG, "Dump finished.");
73         return DumpFileProvider.AUTHORITY_URI.buildUpon().appendPath(fileName).build();
74     }
75 
76     /** @return long random string for a file name */
generateRandomName()77     private static String generateRandomName() {
78         final SecureRandom rng = new SecureRandom();
79         final byte[] random = new byte[256 / 8];
80         rng.nextBytes(random);
81 
82         return Hex.encodeHex(random, true);
83     }
84 
ensureValidFileName(String fileName)85     public static void ensureValidFileName(String fileName) {
86         // Do not allow queries to use relative paths to leave the root directory. Otherwise they
87         // can gain access to other files such as the contacts database.
88         if (fileName.contains("..")) {
89             throw new IllegalArgumentException(".. path specifier not allowed. Bad file name: " +
90                     fileName);
91         }
92         // White list dump files.
93         if (!fileName.matches(VALID_FILE_NAME_REGEX)) {
94             throw new IllegalArgumentException("Only " + VALID_FILE_NAME_REGEX +
95                     " files are supported. Bad file name: " + fileName);
96         }
97     }
98 
getOutputDirectory(Context context)99     private static File getOutputDirectory(Context context) {
100         return new File(context.getCacheDir(), DUMP_FILE_DIRECTORY_NAME);
101     }
102 
ensureOutputDirectory(Context context)103     private static void ensureOutputDirectory(Context context) {
104         final File directory = getOutputDirectory(context);
105         if (!directory.exists()) {
106             directory.mkdir();
107         }
108     }
109 
getOutputFile(Context context, String fileName)110     public static File getOutputFile(Context context, String fileName) {
111         return new File(getOutputDirectory(context), fileName);
112     }
113 
dumpFileExists(Context context)114     public static boolean dumpFileExists(Context context) {
115         return getOutputDirectory(context).exists();
116     }
117 
removeDumpFiles(Context context)118     public static void removeDumpFiles(Context context) {
119         removeFileOrDirectory(getOutputDirectory(context));
120     }
121 
removeFileOrDirectory(File file)122     private static void removeFileOrDirectory(File file) {
123         if (!file.exists()) return;
124 
125         if (file.isFile()) {
126             Log.i(TAG, "Removing " + file);
127             file.delete();
128             return;
129         }
130 
131         if (file.isDirectory()) {
132             for (File child : file.listFiles()) {
133                 removeFileOrDirectory(child);
134             }
135             Log.i(TAG, "Removing " + file);
136             file.delete();
137         }
138     }
139 
140     /**
141      * Add all files under {@code current} to {@code os} zip stream
142      */
addDirectory(Context context, ZipOutputStream os, File current, String storedPath)143     private static void addDirectory(Context context, ZipOutputStream os, File current,
144             String storedPath) throws IOException {
145         for (File child : current.listFiles()) {
146             final String childStoredPath = storedPath + "/" + child.getName();
147 
148             if (child.isDirectory()) {
149                 // Don't need the cache directory, which also contains the dump files.
150                 if (child.equals(context.getCacheDir())) {
151                     continue;
152                 }
153                 // This check is redundant as the output directory should be in the cache dir,
154                 // but just in case...
155                 if (child.getName().equals(DUMP_FILE_DIRECTORY_NAME)) {
156                     continue;
157                 }
158                 addDirectory(context, os, child, childStoredPath);
159             } else if (child.isFile()) {
160                 addFile(os, child, childStoredPath);
161             } else {
162                 // Shouldn't happen; skip.
163             }
164         }
165     }
166 
167     /**
168      * Add a single file {@code current} to {@code os} zip stream using the file name
169      * {@code storedPath}.
170      */
addFile(ZipOutputStream os, File current, String storedPath)171     private static void addFile(ZipOutputStream os, File current, String storedPath)
172             throws IOException {
173         Log.i(TAG, "Adding " + current.getAbsolutePath() + " ...");
174         final InputStream is = new FileInputStream(current);
175         os.putNextEntry(new ZipEntry(storedPath));
176 
177         final byte[] buf = new byte[32 * 1024];
178         int totalLen = 0;
179         while (true) {
180             int len = is.read(buf);
181             if (len <= 0) {
182                 break;
183             }
184             os.write(buf, 0, len);
185             totalLen += len;
186         }
187         os.closeEntry();
188         Log.i(TAG, "Added " + current.getAbsolutePath() + " as " + storedPath +
189                 " (" + totalLen + " bytes)");
190     }
191 }
192