LocalTransport.java revision 4dd2635bf501ad1a1adc22a6ceb4c66cd61a1a23
1/* 2 * Copyright (C) 2009 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.internal.backup; 18 19import android.app.backup.BackupDataInput; 20import android.app.backup.BackupDataOutput; 21import android.app.backup.BackupTransport; 22import android.app.backup.RestoreSet; 23import android.content.ComponentName; 24import android.content.Context; 25import android.content.Intent; 26import android.content.pm.PackageInfo; 27import android.os.Environment; 28import android.os.ParcelFileDescriptor; 29import android.os.SELinux; 30import android.system.ErrnoException; 31import android.system.Os; 32import android.system.StructStat; 33import android.util.Log; 34 35import com.android.org.bouncycastle.util.encoders.Base64; 36 37import java.io.File; 38import java.io.FileInputStream; 39import java.io.FileOutputStream; 40import java.io.IOException; 41import java.util.ArrayList; 42import java.util.Collections; 43 44import static android.system.OsConstants.*; 45 46/** 47 * Backup transport for stashing stuff into a known location on disk, and 48 * later restoring from there. For testing only. 49 */ 50 51public class LocalTransport extends BackupTransport { 52 private static final String TAG = "LocalTransport"; 53 private static final boolean DEBUG = true; 54 55 private static final String TRANSPORT_DIR_NAME 56 = "com.android.internal.backup.LocalTransport"; 57 58 private static final String TRANSPORT_DESTINATION_STRING 59 = "Backing up to debug-only private cache"; 60 61 // The currently-active restore set always has the same (nonzero!) token 62 private static final long CURRENT_SET_TOKEN = 1; 63 64 private Context mContext; 65 private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup"); 66 private File mCurrentSetDir = new File(mDataDir, Long.toString(CURRENT_SET_TOKEN)); 67 68 private PackageInfo[] mRestorePackages = null; 69 private int mRestorePackage = -1; // Index into mRestorePackages 70 private File mRestoreDataDir; 71 private long mRestoreToken; 72 73 74 public LocalTransport(Context context) { 75 mContext = context; 76 mCurrentSetDir.mkdirs(); 77 if (!SELinux.restorecon(mCurrentSetDir)) { 78 Log.e(TAG, "SELinux restorecon failed for " + mCurrentSetDir); 79 } 80 } 81 82 public String name() { 83 return new ComponentName(mContext, this.getClass()).flattenToShortString(); 84 } 85 86 public Intent configurationIntent() { 87 // The local transport is not user-configurable 88 return null; 89 } 90 91 public String currentDestinationString() { 92 return TRANSPORT_DESTINATION_STRING; 93 } 94 95 public String transportDirName() { 96 return TRANSPORT_DIR_NAME; 97 } 98 99 public long requestBackupTime() { 100 // any time is a good time for local backup 101 return 0; 102 } 103 104 public int initializeDevice() { 105 if (DEBUG) Log.v(TAG, "wiping all data"); 106 deleteContents(mCurrentSetDir); 107 return BackupTransport.TRANSPORT_OK; 108 } 109 110 public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) { 111 if (DEBUG) { 112 try { 113 StructStat ss = Os.fstat(data.getFileDescriptor()); 114 Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName 115 + " size=" + ss.st_size); 116 } catch (ErrnoException e) { 117 Log.w(TAG, "Unable to stat input file in performBackup() on " 118 + packageInfo.packageName); 119 } 120 } 121 122 File packageDir = new File(mCurrentSetDir, packageInfo.packageName); 123 packageDir.mkdirs(); 124 125 // Each 'record' in the restore set is kept in its own file, named by 126 // the record key. Wind through the data file, extracting individual 127 // record operations and building a set of all the updates to apply 128 // in this update. 129 BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor()); 130 try { 131 int bufSize = 512; 132 byte[] buf = new byte[bufSize]; 133 while (changeSet.readNextHeader()) { 134 String key = changeSet.getKey(); 135 String base64Key = new String(Base64.encode(key.getBytes())); 136 File entityFile = new File(packageDir, base64Key); 137 138 int dataSize = changeSet.getDataSize(); 139 140 if (DEBUG) Log.v(TAG, "Got change set key=" + key + " size=" + dataSize 141 + " key64=" + base64Key); 142 143 if (dataSize >= 0) { 144 if (entityFile.exists()) { 145 entityFile.delete(); 146 } 147 FileOutputStream entity = new FileOutputStream(entityFile); 148 149 if (dataSize > bufSize) { 150 bufSize = dataSize; 151 buf = new byte[bufSize]; 152 } 153 changeSet.readEntityData(buf, 0, dataSize); 154 if (DEBUG) { 155 try { 156 long cur = Os.lseek(data.getFileDescriptor(), 0, SEEK_CUR); 157 Log.v(TAG, " read entity data; new pos=" + cur); 158 } 159 catch (ErrnoException e) { 160 Log.w(TAG, "Unable to stat input file in performBackup() on " 161 + packageInfo.packageName); 162 } 163 } 164 165 try { 166 entity.write(buf, 0, dataSize); 167 } catch (IOException e) { 168 Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath()); 169 return BackupTransport.TRANSPORT_ERROR; 170 } finally { 171 entity.close(); 172 } 173 } else { 174 entityFile.delete(); 175 } 176 } 177 return BackupTransport.TRANSPORT_OK; 178 } catch (IOException e) { 179 // oops, something went wrong. abort the operation and return error. 180 Log.v(TAG, "Exception reading backup input:", e); 181 return BackupTransport.TRANSPORT_ERROR; 182 } 183 } 184 185 // Deletes the contents but not the given directory 186 private void deleteContents(File dirname) { 187 File[] contents = dirname.listFiles(); 188 if (contents != null) { 189 for (File f : contents) { 190 if (f.isDirectory()) { 191 // delete the directory's contents then fall through 192 // and delete the directory itself. 193 deleteContents(f); 194 } 195 f.delete(); 196 } 197 } 198 } 199 200 public int clearBackupData(PackageInfo packageInfo) { 201 if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName); 202 203 File packageDir = new File(mCurrentSetDir, packageInfo.packageName); 204 final File[] fileset = packageDir.listFiles(); 205 if (fileset != null) { 206 for (File f : fileset) { 207 f.delete(); 208 } 209 packageDir.delete(); 210 } 211 return BackupTransport.TRANSPORT_OK; 212 } 213 214 public int finishBackup() { 215 if (DEBUG) Log.v(TAG, "finishBackup()"); 216 return BackupTransport.TRANSPORT_OK; 217 } 218 219 // Restore handling 220 static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 }; 221 public RestoreSet[] getAvailableRestoreSets() { 222 long[] existing = new long[POSSIBLE_SETS.length + 1]; 223 int num = 0; 224 225 // see which possible non-current sets exist, then put the current set at the end 226 for (long token : POSSIBLE_SETS) { 227 if ((new File(mDataDir, Long.toString(token))).exists()) { 228 existing[num++] = token; 229 } 230 } 231 // and always the currently-active set last 232 existing[num++] = CURRENT_SET_TOKEN; 233 234 RestoreSet[] available = new RestoreSet[num]; 235 for (int i = 0; i < available.length; i++) { 236 available[i] = new RestoreSet("Local disk image", "flash", existing[i]); 237 } 238 return available; 239 } 240 241 public long getCurrentRestoreSet() { 242 // The current restore set always has the same token 243 return CURRENT_SET_TOKEN; 244 } 245 246 public int startRestore(long token, PackageInfo[] packages) { 247 if (DEBUG) Log.v(TAG, "start restore " + token); 248 mRestorePackages = packages; 249 mRestorePackage = -1; 250 mRestoreToken = token; 251 mRestoreDataDir = new File(mDataDir, Long.toString(token)); 252 return BackupTransport.TRANSPORT_OK; 253 } 254 255 public String nextRestorePackage() { 256 if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); 257 while (++mRestorePackage < mRestorePackages.length) { 258 String name = mRestorePackages[mRestorePackage].packageName; 259 // skip packages where we have a data dir but no actual contents 260 String[] contents = (new File(mRestoreDataDir, name)).list(); 261 if (contents != null && contents.length > 0) { 262 if (DEBUG) Log.v(TAG, " nextRestorePackage() = " + name); 263 return name; 264 } 265 } 266 267 if (DEBUG) Log.v(TAG, " no more packages to restore"); 268 return ""; 269 } 270 271 public int getRestoreData(ParcelFileDescriptor outFd) { 272 if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); 273 if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called"); 274 File packageDir = new File(mRestoreDataDir, mRestorePackages[mRestorePackage].packageName); 275 276 // The restore set is the concatenation of the individual record blobs, 277 // each of which is a file in the package's directory. We return the 278 // data in lexical order sorted by key, so that apps which use synthetic 279 // keys like BLOB_1, BLOB_2, etc will see the date in the most obvious 280 // order. 281 ArrayList<DecodedFilename> blobs = contentsByKey(packageDir); 282 if (blobs == null) { // nextRestorePackage() ensures the dir exists, so this is an error 283 Log.e(TAG, "No keys for package: " + packageDir); 284 return BackupTransport.TRANSPORT_ERROR; 285 } 286 287 // We expect at least some data if the directory exists in the first place 288 if (DEBUG) Log.v(TAG, " getRestoreData() found " + blobs.size() + " key files"); 289 BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor()); 290 try { 291 for (DecodedFilename keyEntry : blobs) { 292 File f = keyEntry.file; 293 FileInputStream in = new FileInputStream(f); 294 try { 295 int size = (int) f.length(); 296 byte[] buf = new byte[size]; 297 in.read(buf); 298 if (DEBUG) Log.v(TAG, " ... key=" + keyEntry.key + " size=" + size); 299 out.writeEntityHeader(keyEntry.key, size); 300 out.writeEntityData(buf, size); 301 } finally { 302 in.close(); 303 } 304 } 305 return BackupTransport.TRANSPORT_OK; 306 } catch (IOException e) { 307 Log.e(TAG, "Unable to read backup records", e); 308 return BackupTransport.TRANSPORT_ERROR; 309 } 310 } 311 312 static class DecodedFilename implements Comparable<DecodedFilename> { 313 public File file; 314 public String key; 315 316 public DecodedFilename(File f) { 317 file = f; 318 key = new String(Base64.decode(f.getName())); 319 } 320 321 @Override 322 public int compareTo(DecodedFilename other) { 323 // sorts into ascending lexical order by decoded key 324 return key.compareTo(other.key); 325 } 326 } 327 328 // Return a list of the files in the given directory, sorted lexically by 329 // the Base64-decoded file name, not by the on-disk filename 330 private ArrayList<DecodedFilename> contentsByKey(File dir) { 331 File[] allFiles = dir.listFiles(); 332 if (allFiles == null || allFiles.length == 0) { 333 return null; 334 } 335 336 // Decode the filenames into keys then sort lexically by key 337 ArrayList<DecodedFilename> contents = new ArrayList<DecodedFilename>(); 338 for (File f : allFiles) { 339 contents.add(new DecodedFilename(f)); 340 } 341 Collections.sort(contents); 342 return contents; 343 } 344 345 public void finishRestore() { 346 if (DEBUG) Log.v(TAG, "finishRestore()"); 347 } 348} 349