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 
17 package com.android.localtransport;
18 
19 import android.annotation.Nullable;
20 import android.app.backup.BackupAgent;
21 import android.app.backup.BackupDataInput;
22 import android.app.backup.BackupDataOutput;
23 import android.app.backup.BackupTransport;
24 import android.app.backup.RestoreDescription;
25 import android.app.backup.RestoreSet;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageInfo;
30 import android.os.ParcelFileDescriptor;
31 import android.system.ErrnoException;
32 import android.system.Os;
33 import android.system.StructStat;
34 import android.util.ArrayMap;
35 import android.util.Base64;
36 import android.util.Log;
37 
38 import libcore.io.IoUtils;
39 
40 import java.io.BufferedOutputStream;
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileNotFoundException;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.util.ArrayList;
47 import java.util.Collections;
48 
49 /**
50  * Backup transport for stashing stuff into a known location on disk, and
51  * later restoring from there.  For testing only.
52  */
53 
54 public class LocalTransport extends BackupTransport {
55     private static final String TAG = "LocalTransport";
56     private static final boolean DEBUG = false;
57 
58     private static final String TRANSPORT_DIR_NAME
59             = "com.android.localtransport.LocalTransport";
60 
61     private static final String TRANSPORT_DESTINATION_STRING
62             = "Backing up to debug-only private cache";
63 
64     private static final String TRANSPORT_DATA_MANAGEMENT_LABEL
65             = "";
66 
67     private static final String INCREMENTAL_DIR = "_delta";
68     private static final String FULL_DATA_DIR = "_full";
69     private static final String DEVICE_NAME_FOR_D2D_RESTORE_SET = "D2D";
70     private static final String DEFAULT_DEVICE_NAME_FOR_RESTORE_SET = "flash";
71 
72     // The currently-active restore set always has the same (nonzero!) token
73     private static final long CURRENT_SET_TOKEN = 1;
74 
75     // Size quotas at reasonable values, similar to the current cloud-storage limits
76     private static final long FULL_BACKUP_SIZE_QUOTA = 25 * 1024 * 1024;
77     protected static final long KEY_VALUE_BACKUP_SIZE_QUOTA = 5 * 1024 * 1024;
78 
79     private Context mContext;
80     private File mDataDir;
81     private File mCurrentSetDir;
82     protected File mCurrentSetIncrementalDir;
83     private File mCurrentSetFullDir;
84 
85     protected PackageInfo[] mRestorePackages = null;
86     protected int mRestorePackage = -1;  // Index into mRestorePackages
87     protected int mRestoreType;
88     private File mRestoreSetDir;
89     protected File mRestoreSetIncrementalDir;
90     private File mRestoreSetFullDir;
91 
92     // Additional bookkeeping for full backup
93     private String mFullTargetPackage;
94     private ParcelFileDescriptor mSocket;
95     private FileInputStream mSocketInputStream;
96     private BufferedOutputStream mFullBackupOutputStream;
97     private byte[] mFullBackupBuffer;
98     private long mFullBackupSize;
99 
100     private FileInputStream mCurFullRestoreStream;
101     private byte[] mFullRestoreBuffer;
102     private final LocalTransportParameters mParameters;
103 
makeDataDirs()104     private void makeDataDirs() {
105         mDataDir = mContext.getFilesDir();
106         mCurrentSetDir = new File(mDataDir, Long.toString(CURRENT_SET_TOKEN));
107         mCurrentSetIncrementalDir = new File(mCurrentSetDir, INCREMENTAL_DIR);
108         mCurrentSetFullDir = new File(mCurrentSetDir, FULL_DATA_DIR);
109 
110         mCurrentSetDir.mkdirs();
111         mCurrentSetFullDir.mkdir();
112         mCurrentSetIncrementalDir.mkdir();
113     }
114 
LocalTransport(Context context, LocalTransportParameters parameters)115     public LocalTransport(Context context, LocalTransportParameters parameters) {
116         mContext = context;
117         mParameters = parameters;
118         makeDataDirs();
119     }
120 
getParameters()121     public LocalTransportParameters getParameters() {
122         return mParameters;
123     }
124 
125     @Override
name()126     public String name() {
127         return new ComponentName(mContext, this.getClass()).flattenToShortString();
128     }
129 
130     @Override
configurationIntent()131     public Intent configurationIntent() {
132         // The local transport is not user-configurable
133         return null;
134     }
135 
136     @Override
currentDestinationString()137     public String currentDestinationString() {
138         return TRANSPORT_DESTINATION_STRING;
139     }
140 
dataManagementIntent()141     public Intent dataManagementIntent() {
142         // The local transport does not present a data-management UI
143         // TODO: consider adding simple UI to wipe the archives entirely,
144         // for cleaning up the cache partition.
145         return null;
146     }
147 
148     @Override
149     @Nullable
dataManagementIntentLabel()150     public CharSequence dataManagementIntentLabel() {
151         return TRANSPORT_DATA_MANAGEMENT_LABEL;
152     }
153 
154     @Override
transportDirName()155     public String transportDirName() {
156         return TRANSPORT_DIR_NAME;
157     }
158 
159     @Override
getTransportFlags()160     public int getTransportFlags() {
161         int flags = super.getTransportFlags();
162         // Testing for a fake flag and having it set as a boolean in settings prevents anyone from
163         // using this it to pull data from the agent
164         if (mParameters.isFakeEncryptionFlag()) {
165             flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED;
166         }
167         if (mParameters.isDeviceTransfer()) {
168             flags |= BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER;
169         }
170         if (mParameters.isEncrypted()) {
171             flags |= BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
172         }
173         return flags;
174     }
175 
176     @Override
requestBackupTime()177     public long requestBackupTime() {
178         // any time is a good time for local backup
179         return 0;
180     }
181 
182     @Override
initializeDevice()183     public int initializeDevice() {
184         if (DEBUG) Log.v(TAG, "wiping all data");
185         deleteContents(mCurrentSetDir);
186         makeDataDirs();
187         return TRANSPORT_OK;
188     }
189 
190     // Encapsulation of a single k/v element change
191     private class KVOperation {
192         final String key;     // Element filename, not the raw key, for efficiency
193         final byte[] value;   // null when this is a deletion operation
194 
KVOperation(String k, byte[] v)195         KVOperation(String k, byte[] v) {
196             key = k;
197             value = v;
198         }
199     }
200 
201     @Override
performBackup(PackageInfo packageInfo, ParcelFileDescriptor data)202     public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) {
203         return performBackup(packageInfo, data, /*flags=*/ 0);
204     }
205 
206     @Override
performBackup(PackageInfo packageInfo, ParcelFileDescriptor data, int flags)207     public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data, int flags) {
208         try {
209             return performBackupInternal(packageInfo, data, flags);
210         } finally {
211             IoUtils.closeQuietly(data);
212         }
213     }
214 
performBackupInternal( PackageInfo packageInfo, ParcelFileDescriptor data, int flags)215     private int performBackupInternal(
216             PackageInfo packageInfo, ParcelFileDescriptor data, int flags) {
217         if ((flags & BackupTransport.FLAG_DATA_NOT_CHANGED) != 0) {
218             // For unchanged data notifications we do nothing and tell the
219             // caller everything was OK
220             return BackupTransport.TRANSPORT_OK;
221         }
222 
223         boolean isIncremental = (flags & FLAG_INCREMENTAL) != 0;
224         boolean isNonIncremental = (flags & FLAG_NON_INCREMENTAL) != 0;
225 
226         if (isIncremental) {
227             Log.i(TAG, "Performing incremental backup for " + packageInfo.packageName);
228         } else if (isNonIncremental) {
229             Log.i(TAG, "Performing non-incremental backup for " + packageInfo.packageName);
230         } else {
231             Log.i(TAG, "Performing backup for " + packageInfo.packageName);
232         }
233 
234         if (DEBUG) {
235             try {
236                 StructStat ss = Os.fstat(data.getFileDescriptor());
237                 Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName
238                         + " size=" + ss.st_size + " flags=" + flags);
239             } catch (ErrnoException e) {
240                 Log.w(TAG, "Unable to stat input file in performBackup() on "
241                         + packageInfo.packageName);
242             }
243         }
244 
245         File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName);
246         boolean hasDataForPackage = !packageDir.mkdirs();
247 
248         if (isIncremental) {
249             if (mParameters.isNonIncrementalOnly() || !hasDataForPackage) {
250                 if (mParameters.isNonIncrementalOnly()) {
251                     Log.w(TAG, "Transport is in non-incremental only mode.");
252 
253                 } else {
254                     Log.w(TAG,
255                             "Requested incremental, but transport currently stores no data for the "
256                                     + "package, requesting non-incremental retry.");
257                 }
258                 return TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED;
259             }
260         }
261         if (isNonIncremental && hasDataForPackage) {
262             Log.w(TAG, "Requested non-incremental, deleting existing data.");
263             clearBackupData(packageInfo);
264             packageDir.mkdirs();
265         }
266 
267         // Each 'record' in the restore set is kept in its own file, named by
268         // the record key.  Wind through the data file, extracting individual
269         // record operations and building a list of all the updates to apply
270         // in this update.
271         final ArrayList<KVOperation> changeOps;
272         try {
273             changeOps = parseBackupStream(data);
274         } catch (IOException e) {
275             // oops, something went wrong.  abort the operation and return error.
276             Log.v(TAG, "Exception reading backup input", e);
277             return TRANSPORT_ERROR;
278         }
279 
280         // Okay, now we've parsed out the delta's individual operations.  We need to measure
281         // the effect against what we already have in the datastore to detect quota overrun.
282         // So, we first need to tally up the current in-datastore size per key.
283         final ArrayMap<String, Integer> datastore = new ArrayMap<>();
284         int totalSize = parseKeySizes(packageDir, datastore);
285 
286         // ... and now figure out the datastore size that will result from applying the
287         // sequence of delta operations
288         if (DEBUG) {
289             if (changeOps.size() > 0) {
290                 Log.v(TAG, "Calculating delta size impact");
291             } else {
292                 Log.v(TAG, "No operations in backup stream, so no size change");
293             }
294         }
295         int updatedSize = totalSize;
296         for (KVOperation op : changeOps) {
297             // Deduct the size of the key we're about to replace, if any
298             final Integer curSize = datastore.get(op.key);
299             if (curSize != null) {
300                 updatedSize -= curSize.intValue();
301                 if (DEBUG && op.value == null) {
302                     Log.v(TAG, "  delete " + op.key + ", updated total " + updatedSize);
303                 }
304             }
305 
306             // And add back the size of the value we're about to store, if any
307             if (op.value != null) {
308                 updatedSize += op.value.length;
309                 if (DEBUG) {
310                     Log.v(TAG, ((curSize == null) ? "  new " : "  replace ")
311                             +  op.key + ", updated total " + updatedSize);
312                 }
313             }
314         }
315 
316         // If our final size is over quota, report the failure
317         if (updatedSize > KEY_VALUE_BACKUP_SIZE_QUOTA) {
318             if (DEBUG) {
319                 Log.i(TAG, "New datastore size " + updatedSize
320                         + " exceeds quota " + KEY_VALUE_BACKUP_SIZE_QUOTA);
321             }
322             return TRANSPORT_QUOTA_EXCEEDED;
323         }
324 
325         // No problem with storage size, so go ahead and apply the delta operations
326         // (in the order that the app provided them)
327         for (KVOperation op : changeOps) {
328             File element = new File(packageDir, op.key);
329 
330             // this is either a deletion or a rewrite-from-zero, so we can just remove
331             // the existing file and proceed in either case.
332             element.delete();
333 
334             // if this wasn't a deletion, put the new data in place
335             if (op.value != null) {
336                 try (FileOutputStream out = new FileOutputStream(element)) {
337                     out.write(op.value, 0, op.value.length);
338                 } catch (IOException e) {
339                     Log.e(TAG, "Unable to update key file " + element, e);
340                     return TRANSPORT_ERROR;
341                 }
342             }
343         }
344         return TRANSPORT_OK;
345     }
346 
347     // Parses a backup stream into individual key/value operations
parseBackupStream(ParcelFileDescriptor data)348     private ArrayList<KVOperation> parseBackupStream(ParcelFileDescriptor data)
349             throws IOException {
350         ArrayList<KVOperation> changeOps = new ArrayList<>();
351         BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor());
352         while (changeSet.readNextHeader()) {
353             String key = changeSet.getKey();
354             String base64Key = new String(Base64.encode(key.getBytes(), Base64.NO_WRAP));
355             int dataSize = changeSet.getDataSize();
356             if (DEBUG) {
357                 Log.v(TAG, "  Delta operation key " + key + "   size " + dataSize
358                         + "   key64 " + base64Key);
359             }
360 
361             byte[] buf = (dataSize >= 0) ? new byte[dataSize] : null;
362             if (dataSize >= 0) {
363                 changeSet.readEntityData(buf, 0, dataSize);
364             }
365             changeOps.add(new KVOperation(base64Key, buf));
366         }
367         return changeOps;
368     }
369 
370     // Reads the given datastore directory, building a table of the value size of each
371     // keyed element, and returning the summed total.
parseKeySizes(File packageDir, ArrayMap<String, Integer> datastore)372     private int parseKeySizes(File packageDir, ArrayMap<String, Integer> datastore) {
373         int totalSize = 0;
374         final String[] elements = packageDir.list();
375         if (elements != null) {
376             if (DEBUG) {
377                 Log.v(TAG, "Existing datastore contents:");
378             }
379             for (String file : elements) {
380                 File element = new File(packageDir, file);
381                 String key = file;  // filename
382                 int size = (int) element.length();
383                 totalSize += size;
384                 if (DEBUG) {
385                     Log.v(TAG, "  key " + key + "   size " + size);
386                 }
387                 datastore.put(key, size);
388             }
389             if (DEBUG) {
390                 Log.v(TAG, "  TOTAL: " + totalSize);
391             }
392         } else {
393             if (DEBUG) {
394                 Log.v(TAG, "No existing data for this package");
395             }
396         }
397         return totalSize;
398     }
399 
400     // Deletes the contents but not the given directory
deleteContents(File dirname)401     private void deleteContents(File dirname) {
402         File[] contents = dirname.listFiles();
403         if (contents != null) {
404             for (File f : contents) {
405                 if (f.isDirectory()) {
406                     // delete the directory's contents then fall through
407                     // and delete the directory itself.
408                     deleteContents(f);
409                 }
410                 f.delete();
411             }
412         }
413     }
414 
415     @Override
clearBackupData(PackageInfo packageInfo)416     public int clearBackupData(PackageInfo packageInfo) {
417         if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName);
418 
419         File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName);
420         final File[] fileset = packageDir.listFiles();
421         if (fileset != null) {
422             for (File f : fileset) {
423                 f.delete();
424             }
425             packageDir.delete();
426         }
427 
428         packageDir = new File(mCurrentSetFullDir, packageInfo.packageName);
429         final File[] tarballs = packageDir.listFiles();
430         if (tarballs != null) {
431             for (File f : tarballs) {
432                 f.delete();
433             }
434             packageDir.delete();
435         }
436 
437         return TRANSPORT_OK;
438     }
439 
440     @Override
finishBackup()441     public int finishBackup() {
442         if (DEBUG) Log.v(TAG, "finishBackup() of " + mFullTargetPackage);
443         return tearDownFullBackup();
444     }
445 
446     // ------------------------------------------------------------------------------------
447     // Full backup handling
448 
tearDownFullBackup()449     private int tearDownFullBackup() {
450         if (mSocket != null) {
451             try {
452                 if (mFullBackupOutputStream != null) {
453                     mFullBackupOutputStream.flush();
454                     mFullBackupOutputStream.close();
455                 }
456                 mSocketInputStream = null;
457                 mFullTargetPackage = null;
458                 mSocket.close();
459             } catch (IOException e) {
460                 if (DEBUG) {
461                     Log.w(TAG, "Exception caught in tearDownFullBackup()", e);
462                 }
463                 return TRANSPORT_ERROR;
464             } finally {
465                 mSocket = null;
466                 mFullBackupOutputStream = null;
467             }
468         }
469         return TRANSPORT_OK;
470     }
471 
tarballFile(String pkgName)472     private File tarballFile(String pkgName) {
473         return new File(mCurrentSetFullDir, pkgName);
474     }
475 
476     @Override
requestFullBackupTime()477     public long requestFullBackupTime() {
478         return 0;
479     }
480 
481     @Override
checkFullBackupSize(long size)482     public int checkFullBackupSize(long size) {
483         int result = TRANSPORT_OK;
484         // Decline zero-size "backups"
485         if (size <= 0) {
486             result = TRANSPORT_PACKAGE_REJECTED;
487         } else if (size > FULL_BACKUP_SIZE_QUOTA) {
488             result = TRANSPORT_QUOTA_EXCEEDED;
489         }
490         if (result != TRANSPORT_OK) {
491             if (DEBUG) {
492                 Log.v(TAG, "Declining backup of size " + size);
493             }
494         }
495         return result;
496     }
497 
498     @Override
performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket)499     public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) {
500         if (mSocket != null) {
501             Log.e(TAG, "Attempt to initiate full backup while one is in progress");
502             return TRANSPORT_ERROR;
503         }
504 
505         if (DEBUG) {
506             Log.i(TAG, "performFullBackup : " + targetPackage);
507         }
508 
509         // We know a priori that we run in the system process, so we need to make
510         // sure to dup() our own copy of the socket fd.  Transports which run in
511         // their own processes must not do this.
512         try {
513             mFullBackupSize = 0;
514             mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor());
515             mSocketInputStream = new FileInputStream(mSocket.getFileDescriptor());
516         } catch (IOException e) {
517             Log.e(TAG, "Unable to process socket for full backup");
518             return TRANSPORT_ERROR;
519         }
520 
521         mFullTargetPackage = targetPackage.packageName;
522         mFullBackupBuffer = new byte[4096];
523 
524         return TRANSPORT_OK;
525     }
526 
527     @Override
sendBackupData(final int numBytes)528     public int sendBackupData(final int numBytes) {
529         if (mSocket == null) {
530             Log.w(TAG, "Attempted sendBackupData before performFullBackup");
531             return TRANSPORT_ERROR;
532         }
533 
534         mFullBackupSize += numBytes;
535         if (mFullBackupSize > FULL_BACKUP_SIZE_QUOTA) {
536             return TRANSPORT_QUOTA_EXCEEDED;
537         }
538 
539         if (numBytes > mFullBackupBuffer.length) {
540             mFullBackupBuffer = new byte[numBytes];
541         }
542 
543         if (mFullBackupOutputStream == null) {
544             FileOutputStream tarstream;
545             try {
546                 File tarball = tarballFile(mFullTargetPackage);
547                 tarstream = new FileOutputStream(tarball);
548             } catch (FileNotFoundException e) {
549                 return TRANSPORT_ERROR;
550             }
551             mFullBackupOutputStream = new BufferedOutputStream(tarstream);
552         }
553 
554         int bytesLeft = numBytes;
555         while (bytesLeft > 0) {
556             try {
557                 int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, bytesLeft);
558                 if (nRead < 0) {
559                     // Something went wrong if we expect data but saw EOD
560                     Log.w(TAG, "Unexpected EOD; failing backup");
561                     return TRANSPORT_ERROR;
562                 }
563                 mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead);
564                 bytesLeft -= nRead;
565             } catch (IOException e) {
566                 Log.e(TAG, "Error handling backup data for " + mFullTargetPackage);
567                 return TRANSPORT_ERROR;
568             }
569         }
570         if (DEBUG) {
571             Log.v(TAG, "   stored " + numBytes + " of data");
572         }
573         return TRANSPORT_OK;
574     }
575 
576     // For now we can't roll back, so just tear everything down.
577     @Override
cancelFullBackup()578     public void cancelFullBackup() {
579         if (DEBUG) {
580             Log.i(TAG, "Canceling full backup of " + mFullTargetPackage);
581         }
582         File archive = tarballFile(mFullTargetPackage);
583         tearDownFullBackup();
584         if (archive.exists()) {
585             archive.delete();
586         }
587     }
588 
589     // ------------------------------------------------------------------------------------
590     // Restore handling
591     static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 };
592 
593     @Override
getAvailableRestoreSets()594     public RestoreSet[] getAvailableRestoreSets() {
595         long[] existing = new long[POSSIBLE_SETS.length + 1];
596         int num = 0;
597 
598         // see which possible non-current sets exist...
599         for (long token : POSSIBLE_SETS) {
600             if ((new File(mDataDir, Long.toString(token))).exists()) {
601                 existing[num++] = token;
602             }
603         }
604         // ...and always the currently-active set last
605         existing[num++] = CURRENT_SET_TOKEN;
606 
607         RestoreSet[] available = new RestoreSet[num];
608         String deviceName = mParameters.isDeviceTransfer() ? DEVICE_NAME_FOR_D2D_RESTORE_SET
609                 : DEFAULT_DEVICE_NAME_FOR_RESTORE_SET;
610         for (int i = 0; i < available.length; i++) {
611             available[i] = new RestoreSet("Local disk image", deviceName, existing[i]);
612         }
613         return available;
614     }
615 
616     @Override
getCurrentRestoreSet()617     public long getCurrentRestoreSet() {
618         // The current restore set always has the same token
619         return CURRENT_SET_TOKEN;
620     }
621 
622     @Override
startRestore(long token, PackageInfo[] packages)623     public int startRestore(long token, PackageInfo[] packages) {
624         if (DEBUG) Log.v(TAG, "start restore " + token + " : " + packages.length
625                 + " matching packages");
626         mRestorePackages = packages;
627         mRestorePackage = -1;
628         mRestoreSetDir = new File(mDataDir, Long.toString(token));
629         mRestoreSetIncrementalDir = new File(mRestoreSetDir, INCREMENTAL_DIR);
630         mRestoreSetFullDir = new File(mRestoreSetDir, FULL_DATA_DIR);
631         return TRANSPORT_OK;
632     }
633 
634     @Override
nextRestorePackage()635     public RestoreDescription nextRestorePackage() {
636         if (DEBUG) {
637             Log.v(TAG, "nextRestorePackage() : mRestorePackage=" + mRestorePackage
638                     + " length=" + mRestorePackages.length);
639         }
640         if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
641 
642         boolean found;
643         while (++mRestorePackage < mRestorePackages.length) {
644             String name = mRestorePackages[mRestorePackage].packageName;
645 
646             // If we have key/value data for this package, deliver that
647             // skip packages where we have a data dir but no actual contents
648             found = hasRestoreDataForPackage(name);
649             if (found) {
650                 mRestoreType = RestoreDescription.TYPE_KEY_VALUE;
651             }
652 
653             if (!found) {
654                 // No key/value data; check for [non-empty] full data
655                 File maybeFullData = new File(mRestoreSetFullDir, name);
656                 if (maybeFullData.length() > 0) {
657                     if (DEBUG) {
658                         Log.v(TAG, "  nextRestorePackage(TYPE_FULL_STREAM) @ "
659                                 + mRestorePackage + " = " + name);
660                     }
661                     mRestoreType = RestoreDescription.TYPE_FULL_STREAM;
662                     mCurFullRestoreStream = null;   // ensure starting from the ground state
663                     found = true;
664                 }
665             }
666 
667             if (found) {
668                 return new RestoreDescription(name, mRestoreType);
669             }
670 
671             if (DEBUG) {
672                 Log.v(TAG, "  ... package @ " + mRestorePackage + " = " + name
673                         + " has no data; skipping");
674             }
675         }
676 
677         if (DEBUG) Log.v(TAG, "  no more packages to restore");
678         return RestoreDescription.NO_MORE_PACKAGES;
679     }
680 
hasRestoreDataForPackage(String packageName)681     protected boolean hasRestoreDataForPackage(String packageName) {
682         String[] contents = (new File(mRestoreSetIncrementalDir, packageName)).list();
683         if (contents != null && contents.length > 0) {
684             if (DEBUG) {
685                 Log.v(TAG, "  nextRestorePackage(TYPE_KEY_VALUE) @ "
686                         + mRestorePackage + " = " + packageName);
687             }
688             return true;
689         }
690         return false;
691     }
692 
693     @Override
getRestoreData(ParcelFileDescriptor outFd)694     public int getRestoreData(ParcelFileDescriptor outFd) {
695         if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
696         if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called");
697         if (mRestoreType != RestoreDescription.TYPE_KEY_VALUE) {
698             throw new IllegalStateException("getRestoreData(fd) for non-key/value dataset");
699         }
700         File packageDir = new File(mRestoreSetIncrementalDir,
701                 mRestorePackages[mRestorePackage].packageName);
702 
703         // The restore set is the concatenation of the individual record blobs,
704         // each of which is a file in the package's directory.  We return the
705         // data in lexical order sorted by key, so that apps which use synthetic
706         // keys like BLOB_1, BLOB_2, etc will see the date in the most obvious
707         // order.
708         ArrayList<DecodedFilename> blobs = contentsByKey(packageDir);
709         if (blobs == null) {  // nextRestorePackage() ensures the dir exists, so this is an error
710             Log.e(TAG, "No keys for package: " + packageDir);
711             return TRANSPORT_ERROR;
712         }
713 
714         // We expect at least some data if the directory exists in the first place
715         if (DEBUG) Log.v(TAG, "  getRestoreData() found " + blobs.size() + " key files");
716         BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor());
717         try {
718             for (DecodedFilename keyEntry : blobs) {
719                 File f = keyEntry.file;
720                 FileInputStream in = new FileInputStream(f);
721                 try {
722                     int size = (int) f.length();
723                     byte[] buf = new byte[size];
724                     in.read(buf);
725                     if (DEBUG) Log.v(TAG, "    ... key=" + keyEntry.key + " size=" + size);
726                     out.writeEntityHeader(keyEntry.key, size);
727                     out.writeEntityData(buf, size);
728                 } finally {
729                     in.close();
730                 }
731             }
732             return TRANSPORT_OK;
733         } catch (IOException e) {
734             Log.e(TAG, "Unable to read backup records", e);
735             return TRANSPORT_ERROR;
736         }
737     }
738 
739     static class DecodedFilename implements Comparable<DecodedFilename> {
740         public File file;
741         public String key;
742 
DecodedFilename(File f)743         public DecodedFilename(File f) {
744             file = f;
745             key = new String(Base64.decode(f.getName(), Base64.DEFAULT));
746         }
747 
748         @Override
compareTo(DecodedFilename other)749         public int compareTo(DecodedFilename other) {
750             // sorts into ascending lexical order by decoded key
751             return key.compareTo(other.key);
752         }
753     }
754 
755     // Return a list of the files in the given directory, sorted lexically by
756     // the Base64-decoded file name, not by the on-disk filename
contentsByKey(File dir)757     private ArrayList<DecodedFilename> contentsByKey(File dir) {
758         File[] allFiles = dir.listFiles();
759         if (allFiles == null || allFiles.length == 0) {
760             return null;
761         }
762 
763         // Decode the filenames into keys then sort lexically by key
764         ArrayList<DecodedFilename> contents = new ArrayList<DecodedFilename>();
765         for (File f : allFiles) {
766             contents.add(new DecodedFilename(f));
767         }
768         Collections.sort(contents);
769         return contents;
770     }
771 
772     @Override
finishRestore()773     public void finishRestore() {
774         if (DEBUG) Log.v(TAG, "finishRestore()");
775         if (mRestoreType == RestoreDescription.TYPE_FULL_STREAM) {
776             resetFullRestoreState();
777         }
778         mRestoreType = 0;
779     }
780 
781     // ------------------------------------------------------------------------------------
782     // Full restore handling
783 
resetFullRestoreState()784     private void resetFullRestoreState() {
785         IoUtils.closeQuietly(mCurFullRestoreStream);
786         mCurFullRestoreStream = null;
787         mFullRestoreBuffer = null;
788     }
789 
790     /**
791      * Ask the transport to provide data for the "current" package being restored.  The
792      * transport then writes some data to the socket supplied to this call, and returns
793      * the number of bytes written.  The system will then read that many bytes and
794      * stream them to the application's agent for restore, then will call this method again
795      * to receive the next chunk of the archive.  This sequence will be repeated until the
796      * transport returns zero indicating that all of the package's data has been delivered
797      * (or returns a negative value indicating some sort of hard error condition at the
798      * transport level).
799      *
800      * <p>After this method returns zero, the system will then call
801      * {@link #getNextFullRestorePackage()} to begin the restore process for the next
802      * application, and the sequence begins again.
803      *
804      * @param socket The file descriptor that the transport will use for delivering the
805      *    streamed archive.
806      * @return 0 when no more data for the current package is available.  A positive value
807      *    indicates the presence of that much data to be delivered to the app.  A negative
808      *    return value is treated as equivalent to {@link BackupTransport#TRANSPORT_ERROR},
809      *    indicating a fatal error condition that precludes further restore operations
810      *    on the current dataset.
811      */
812     @Override
getNextFullRestoreDataChunk(ParcelFileDescriptor socket)813     public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
814         if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) {
815             throw new IllegalStateException("Asked for full restore data for non-stream package");
816         }
817 
818         // first chunk?
819         if (mCurFullRestoreStream == null) {
820             final String name = mRestorePackages[mRestorePackage].packageName;
821             if (DEBUG) Log.i(TAG, "Starting full restore of " + name);
822             File dataset = new File(mRestoreSetFullDir, name);
823             try {
824                 mCurFullRestoreStream = new FileInputStream(dataset);
825             } catch (IOException e) {
826                 // If we can't open the target package's tarball, we return the single-package
827                 // error code and let the caller go on to the next package.
828                 Log.e(TAG, "Unable to read archive for " + name);
829                 return TRANSPORT_PACKAGE_REJECTED;
830             }
831             mFullRestoreBuffer = new byte[2*1024];
832         }
833 
834         FileOutputStream stream = new FileOutputStream(socket.getFileDescriptor());
835 
836         int nRead;
837         try {
838             nRead = mCurFullRestoreStream.read(mFullRestoreBuffer);
839             if (nRead < 0) {
840                 // EOF: tell the caller we're done
841                 nRead = NO_MORE_DATA;
842             } else if (nRead == 0) {
843                 // This shouldn't happen when reading a FileInputStream; we should always
844                 // get either a positive nonzero byte count or -1.  Log the situation and
845                 // treat it as EOF.
846                 Log.w(TAG, "read() of archive file returned 0; treating as EOF");
847                 nRead = NO_MORE_DATA;
848             } else {
849                 if (DEBUG) {
850                     Log.i(TAG, "   delivering restore chunk: " + nRead);
851                 }
852                 stream.write(mFullRestoreBuffer, 0, nRead);
853             }
854         } catch (IOException e) {
855             return TRANSPORT_ERROR;  // Hard error accessing the file; shouldn't happen
856         } finally {
857             IoUtils.closeQuietly(socket);
858         }
859 
860         return nRead;
861     }
862 
863     /**
864      * If the OS encounters an error while processing {@link RestoreDescription#TYPE_FULL_STREAM}
865      * data for restore, it will invoke this method to tell the transport that it should
866      * abandon the data download for the current package.  The OS will then either call
867      * {@link #nextRestorePackage()} again to move on to restoring the next package in the
868      * set being iterated over, or will call {@link #finishRestore()} to shut down the restore
869      * operation.
870      *
871      * @return {@link #TRANSPORT_OK} if the transport was successful in shutting down the
872      *    current stream cleanly, or {@link #TRANSPORT_ERROR} to indicate a serious
873      *    transport-level failure.  If the transport reports an error here, the entire restore
874      *    operation will immediately be finished with no further attempts to restore app data.
875      */
876     @Override
abortFullRestore()877     public int abortFullRestore() {
878         if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) {
879             throw new IllegalStateException("abortFullRestore() but not currently restoring");
880         }
881         resetFullRestoreState();
882         mRestoreType = 0;
883         return TRANSPORT_OK;
884     }
885 
886     @Override
getBackupQuota(String packageName, boolean isFullBackup)887     public long getBackupQuota(String packageName, boolean isFullBackup) {
888         return isFullBackup ? FULL_BACKUP_SIZE_QUOTA : KEY_VALUE_BACKUP_SIZE_QUOTA;
889     }
890 }
891