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        final File[] fileset = packageDir.listFiles();
170        if (fileset != null) {
171            for (File f : fileset) {
172                f.delete();
173            }
174            packageDir.delete();
175        }
176        return BackupConstants.TRANSPORT_OK;
177    }
178
179    public int finishBackup() {
180        if (DEBUG) Log.v(TAG, "finishBackup()");
181        return BackupConstants.TRANSPORT_OK;
182    }
183
184    // Restore handling
185    public RestoreSet[] getAvailableRestoreSets() throws android.os.RemoteException {
186        // one hardcoded restore set
187        RestoreSet set = new RestoreSet("Local disk image", "flash", RESTORE_TOKEN);
188        RestoreSet[] array = { set };
189        return array;
190    }
191
192    public long getCurrentRestoreSet() {
193        // The hardcoded restore set always has the same token
194        return RESTORE_TOKEN;
195    }
196
197    public int startRestore(long token, PackageInfo[] packages) {
198        if (DEBUG) Log.v(TAG, "start restore " + token);
199        mRestorePackages = packages;
200        mRestorePackage = -1;
201        return BackupConstants.TRANSPORT_OK;
202    }
203
204    public String nextRestorePackage() {
205        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
206        while (++mRestorePackage < mRestorePackages.length) {
207            String name = mRestorePackages[mRestorePackage].packageName;
208            if (new File(mDataDir, name).isDirectory()) {
209                if (DEBUG) Log.v(TAG, "  nextRestorePackage() = " + name);
210                return name;
211            }
212        }
213
214        if (DEBUG) Log.v(TAG, "  no more packages to restore");
215        return "";
216    }
217
218    public int getRestoreData(ParcelFileDescriptor outFd) {
219        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
220        if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called");
221        File packageDir = new File(mDataDir, mRestorePackages[mRestorePackage].packageName);
222
223        // The restore set is the concatenation of the individual record blobs,
224        // each of which is a file in the package's directory
225        File[] blobs = packageDir.listFiles();
226        if (blobs == null) {  // nextRestorePackage() ensures the dir exists, so this is an error
227            Log.e(TAG, "Error listing directory: " + packageDir);
228            return BackupConstants.TRANSPORT_ERROR;
229        }
230
231        // We expect at least some data if the directory exists in the first place
232        if (DEBUG) Log.v(TAG, "  getRestoreData() found " + blobs.length + " key files");
233        BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor());
234        try {
235            for (File f : blobs) {
236                FileInputStream in = new FileInputStream(f);
237                try {
238                    int size = (int) f.length();
239                    byte[] buf = new byte[size];
240                    in.read(buf);
241                    String key = new String(Base64.decode(f.getName()));
242                    if (DEBUG) Log.v(TAG, "    ... key=" + key + " size=" + size);
243                    out.writeEntityHeader(key, size);
244                    out.writeEntityData(buf, size);
245                } finally {
246                    in.close();
247                }
248            }
249            return BackupConstants.TRANSPORT_OK;
250        } catch (IOException e) {
251            Log.e(TAG, "Unable to read backup records", e);
252            return BackupConstants.TRANSPORT_ERROR;
253        }
254    }
255
256    public void finishRestore() {
257        if (DEBUG) Log.v(TAG, "finishRestore()");
258    }
259}
260