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 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 */ 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 */ 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 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 99 private static File getOutputDirectory(Context context) { 100 return new File(context.getCacheDir(), DUMP_FILE_DIRECTORY_NAME); 101 } 102 103 private static void ensureOutputDirectory(Context context) { 104 final File directory = getOutputDirectory(context); 105 if (!directory.exists()) { 106 directory.mkdir(); 107 } 108 } 109 110 public static File getOutputFile(Context context, String fileName) { 111 return new File(getOutputDirectory(context), fileName); 112 } 113 114 public static boolean dumpFileExists(Context context) { 115 return getOutputDirectory(context).exists(); 116 } 117 118 public static void removeDumpFiles(Context context) { 119 removeFileOrDirectory(getOutputDirectory(context)); 120 } 121 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 */ 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 */ 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