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