blob: 494bc7898e93d1ec06491f6f4e6e95ba9088b5ab [file] [log] [blame]
/*
* 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()");
}
}