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.Context;
23import android.content.Intent;
24import android.content.pm.PackageInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.PackageManager.NameNotFoundException;
27import android.os.Environment;
28import android.os.ParcelFileDescriptor;
29import android.os.RemoteException;
30import android.util.Log;
31
32import com.android.org.bouncycastle.util.encoders.Base64;
33
34import java.io.File;
35import java.io.FileFilter;
36import java.io.FileInputStream;
37import java.io.FileOutputStream;
38import java.io.IOException;
39import java.util.ArrayList;
40
41/**
42 * Backup transport for stashing stuff into a known location on disk, and
43 * later restoring from there.  For testing only.
44 */
45
46public class LocalTransport extends IBackupTransport.Stub {
47    private static final String TAG = "LocalTransport";
48    private static final boolean DEBUG = true;
49
50    private static final String TRANSPORT_DIR_NAME
51            = "com.android.internal.backup.LocalTransport";
52
53    private static final String TRANSPORT_DESTINATION_STRING
54            = "Backing up to debug-only private cache";
55
56    // The single hardcoded restore set always has the same (nonzero!) token
57    private static final long RESTORE_TOKEN = 1;
58
59    private Context mContext;
60    private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup");
61    private PackageInfo[] mRestorePackages = null;
62    private int mRestorePackage = -1;  // Index into mRestorePackages
63
64
65    public LocalTransport(Context context) {
66        mContext = context;
67    }
68
69    public Intent configurationIntent() {
70        // The local transport is not user-configurable
71        return null;
72    }
73
74    public String currentDestinationString() {
75        return TRANSPORT_DESTINATION_STRING;
76    }
77
78    public String transportDirName() {
79        return TRANSPORT_DIR_NAME;
80    }
81
82    public long requestBackupTime() {
83        // any time is a good time for local backup
84        return 0;
85    }
86
87    public int initializeDevice() {
88        if (DEBUG) Log.v(TAG, "wiping all data");
89        deleteContents(mDataDir);
90        return BackupConstants.TRANSPORT_OK;
91    }
92
93    public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) {
94        if (DEBUG) Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName);
95
96        File packageDir = new File(mDataDir, packageInfo.packageName);
97        packageDir.mkdirs();
98
99        // Each 'record' in the restore set is kept in its own file, named by
100        // the record key.  Wind through the data file, extracting individual
101        // record operations and building a set of all the updates to apply
102        // in this update.
103        BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor());
104        try {
105            int bufSize = 512;
106            byte[] buf = new byte[bufSize];
107            while (changeSet.readNextHeader()) {
108                String key = changeSet.getKey();
109                String base64Key = new String(Base64.encode(key.getBytes()));
110                File entityFile = new File(packageDir, base64Key);
111
112                int dataSize = changeSet.getDataSize();
113
114                if (DEBUG) Log.v(TAG, "Got change set key=" + key + " size=" + dataSize
115                        + " key64=" + base64Key);
116
117                if (dataSize >= 0) {
118                    if (entityFile.exists()) {
119                        entityFile.delete();
120                    }
121                    FileOutputStream entity = new FileOutputStream(entityFile);
122
123                    if (dataSize > bufSize) {
124                        bufSize = dataSize;
125                        buf = new byte[bufSize];
126                    }
127                    changeSet.readEntityData(buf, 0, dataSize);
128                    if (DEBUG) Log.v(TAG, "  data size " + dataSize);
129
130                    try {
131                        entity.write(buf, 0, dataSize);
132                    } catch (IOException e) {
133                        Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath());
134                        return BackupConstants.TRANSPORT_ERROR;
135                    } finally {
136                        entity.close();
137                    }
138                } else {
139                    entityFile.delete();
140                }
141            }
142            return BackupConstants.TRANSPORT_OK;
143        } catch (IOException e) {
144            // oops, something went wrong.  abort the operation and return error.
145            Log.v(TAG, "Exception reading backup input:", e);
146            return BackupConstants.TRANSPORT_ERROR;
147        }
148    }
149
150    // Deletes the contents but not the given directory
151    private void deleteContents(File dirname) {
152        File[] contents = dirname.listFiles();
153        if (contents != null) {
154            for (File f : contents) {
155                if (f.isDirectory()) {
156                    // delete the directory's contents then fall through
157                    // and delete the directory itself.
158                    deleteContents(f);
159                }
160                f.delete();
161            }
162        }
163    }
164
165    public int clearBackupData(PackageInfo packageInfo) {
166        if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName);
167
168        File packageDir = new File(mDataDir, packageInfo.packageName);
169        for (File f : packageDir.listFiles()) {
170            f.delete();
171        }
172        packageDir.delete();
173        return BackupConstants.TRANSPORT_OK;
174    }
175
176    public int finishBackup() {
177        if (DEBUG) Log.v(TAG, "finishBackup()");
178        return BackupConstants.TRANSPORT_OK;
179    }
180
181    // Restore handling
182    public RestoreSet[] getAvailableRestoreSets() throws android.os.RemoteException {
183        // one hardcoded restore set
184        RestoreSet set = new RestoreSet("Local disk image", "flash", RESTORE_TOKEN);
185        RestoreSet[] array = { set };
186        return array;
187    }
188
189    public long getCurrentRestoreSet() {
190        // The hardcoded restore set always has the same token
191        return RESTORE_TOKEN;
192    }
193
194    public int startRestore(long token, PackageInfo[] packages) {
195        if (DEBUG) Log.v(TAG, "start restore " + token);
196        mRestorePackages = packages;
197        mRestorePackage = -1;
198        return BackupConstants.TRANSPORT_OK;
199    }
200
201    public String nextRestorePackage() {
202        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
203        while (++mRestorePackage < mRestorePackages.length) {
204            String name = mRestorePackages[mRestorePackage].packageName;
205            if (new File(mDataDir, name).isDirectory()) {
206                if (DEBUG) Log.v(TAG, "  nextRestorePackage() = " + name);
207                return name;
208            }
209        }
210
211        if (DEBUG) Log.v(TAG, "  no more packages to restore");
212        return "";
213    }
214
215    public int getRestoreData(ParcelFileDescriptor outFd) {
216        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
217        if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called");
218        File packageDir = new File(mDataDir, mRestorePackages[mRestorePackage].packageName);
219
220        // The restore set is the concatenation of the individual record blobs,
221        // each of which is a file in the package's directory
222        File[] blobs = packageDir.listFiles();
223        if (blobs == null) {  // nextRestorePackage() ensures the dir exists, so this is an error
224            Log.e(TAG, "Error listing directory: " + packageDir);
225            return BackupConstants.TRANSPORT_ERROR;
226        }
227
228        // We expect at least some data if the directory exists in the first place
229        if (DEBUG) Log.v(TAG, "  getRestoreData() found " + blobs.length + " key files");
230        BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor());
231        try {
232            for (File f : blobs) {
233                FileInputStream in = new FileInputStream(f);
234                try {
235                    int size = (int) f.length();
236                    byte[] buf = new byte[size];
237                    in.read(buf);
238                    String key = new String(Base64.decode(f.getName()));
239                    if (DEBUG) Log.v(TAG, "    ... key=" + key + " size=" + size);
240                    out.writeEntityHeader(key, size);
241                    out.writeEntityData(buf, size);
242                } finally {
243                    in.close();
244                }
245            }
246            return BackupConstants.TRANSPORT_OK;
247        } catch (IOException e) {
248            Log.e(TAG, "Unable to read backup records", e);
249            return BackupConstants.TRANSPORT_ERROR;
250        }
251    }
252
253    public void finishRestore() {
254        if (DEBUG) Log.v(TAG, "finishRestore()");
255    }
256}
257