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