/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.backup; import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; import android.app.backup.RestoreSet; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.SELinux; import android.util.Log; import com.android.org.bouncycastle.util.encoders.Base64; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; /** * Backup transport for stashing stuff into a known location on disk, and * later restoring from there. For testing only. */ public class LocalTransport extends IBackupTransport.Stub { private static final String TAG = "LocalTransport"; private static final boolean DEBUG = true; private static final String TRANSPORT_DIR_NAME = "com.android.internal.backup.LocalTransport"; private static final String TRANSPORT_DESTINATION_STRING = "Backing up to debug-only private cache"; // The single hardcoded restore set always has the same (nonzero!) token private static final long RESTORE_TOKEN = 1; private Context mContext; private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup"); private PackageInfo[] mRestorePackages = null; private int mRestorePackage = -1; // Index into mRestorePackages public LocalTransport(Context context) { mContext = context; mDataDir.mkdirs(); if (!SELinux.restorecon(mDataDir)) { Log.e(TAG, "SELinux restorecon failed for " + mDataDir); } } public String name() { return new ComponentName(mContext, this.getClass()).flattenToShortString(); } public Intent configurationIntent() { // The local transport is not user-configurable return null; } public String currentDestinationString() { return TRANSPORT_DESTINATION_STRING; } public String transportDirName() { return TRANSPORT_DIR_NAME; } public long requestBackupTime() { // any time is a good time for local backup return 0; } public int initializeDevice() { if (DEBUG) Log.v(TAG, "wiping all data"); deleteContents(mDataDir); return BackupConstants.TRANSPORT_OK; } public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) { if (DEBUG) Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName); File packageDir = new File(mDataDir, packageInfo.packageName); packageDir.mkdirs(); // Each 'record' in the restore set is kept in its own file, named by // the record key. Wind through the data file, extracting individual // record operations and building a set of all the updates to apply // in this update. BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor()); try { int bufSize = 512; byte[] buf = new byte[bufSize]; while (changeSet.readNextHeader()) { String key = changeSet.getKey(); String base64Key = new String(Base64.encode(key.getBytes())); File entityFile = new File(packageDir, base64Key); int dataSize = changeSet.getDataSize(); if (DEBUG) Log.v(TAG, "Got change set key=" + key + " size=" + dataSize + " key64=" + base64Key); if (dataSize >= 0) { if (entityFile.exists()) { entityFile.delete(); } FileOutputStream entity = new FileOutputStream(entityFile); if (dataSize > bufSize) { bufSize = dataSize; buf = new byte[bufSize]; } changeSet.readEntityData(buf, 0, dataSize); if (DEBUG) Log.v(TAG, " data size " + dataSize); try { entity.write(buf, 0, dataSize); } catch (IOException e) { Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath()); return BackupConstants.TRANSPORT_ERROR; } finally { entity.close(); } } else { entityFile.delete(); } } return BackupConstants.TRANSPORT_OK; } catch (IOException e) { // oops, something went wrong. abort the operation and return error. Log.v(TAG, "Exception reading backup input:", e); return BackupConstants.TRANSPORT_ERROR; } } // Deletes the contents but not the given directory private void deleteContents(File dirname) { File[] contents = dirname.listFiles(); if (contents != null) { for (File f : contents) { if (f.isDirectory()) { // delete the directory's contents then fall through // and delete the directory itself. deleteContents(f); } f.delete(); } } } public int clearBackupData(PackageInfo packageInfo) { if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName); File packageDir = new File(mDataDir, packageInfo.packageName); final File[] fileset = packageDir.listFiles(); if (fileset != null) { for (File f : fileset) { f.delete(); } packageDir.delete(); } return BackupConstants.TRANSPORT_OK; } public int finishBackup() { if (DEBUG) Log.v(TAG, "finishBackup()"); return BackupConstants.TRANSPORT_OK; } // Restore handling public RestoreSet[] getAvailableRestoreSets() throws android.os.RemoteException { // one hardcoded restore set RestoreSet set = new RestoreSet("Local disk image", "flash", RESTORE_TOKEN); RestoreSet[] array = { set }; return array; } public long getCurrentRestoreSet() { // The hardcoded restore set always has the same token return RESTORE_TOKEN; } public int startRestore(long token, PackageInfo[] packages) { if (DEBUG) Log.v(TAG, "start restore " + token); mRestorePackages = packages; mRestorePackage = -1; return BackupConstants.TRANSPORT_OK; } public String nextRestorePackage() { if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); while (++mRestorePackage < mRestorePackages.length) { String name = mRestorePackages[mRestorePackage].packageName; if (new File(mDataDir, name).isDirectory()) { if (DEBUG) Log.v(TAG, " nextRestorePackage() = " + name); return name; } } if (DEBUG) Log.v(TAG, " no more packages to restore"); return ""; } public int getRestoreData(ParcelFileDescriptor outFd) { if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called"); File packageDir = new File(mDataDir, mRestorePackages[mRestorePackage].packageName); // The restore set is the concatenation of the individual record blobs, // each of which is a file in the package's directory File[] blobs = packageDir.listFiles(); if (blobs == null) { // nextRestorePackage() ensures the dir exists, so this is an error Log.e(TAG, "Error listing directory: " + packageDir); return BackupConstants.TRANSPORT_ERROR; } // We expect at least some data if the directory exists in the first place if (DEBUG) Log.v(TAG, " getRestoreData() found " + blobs.length + " key files"); BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor()); try { for (File f : blobs) { FileInputStream in = new FileInputStream(f); try { int size = (int) f.length(); byte[] buf = new byte[size]; in.read(buf); String key = new String(Base64.decode(f.getName())); if (DEBUG) Log.v(TAG, " ... key=" + key + " size=" + size); out.writeEntityHeader(key, size); out.writeEntityData(buf, size); } finally { in.close(); } } return BackupConstants.TRANSPORT_OK; } catch (IOException e) { Log.e(TAG, "Unable to read backup records", e); return BackupConstants.TRANSPORT_ERROR; } } public void finishRestore() { if (DEBUG) Log.v(TAG, "finishRestore()"); } }