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