BackupAgent.java revision 78be158ce4b95fa537c6cb60a55dbc9161e53ef1
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 android.app.backup;
18
19import android.app.IBackupAgent;
20import android.app.backup.IBackupManager;
21import android.content.Context;
22import android.content.ContextWrapper;
23import android.content.pm.ApplicationInfo;
24import android.os.Binder;
25import android.os.IBinder;
26import android.os.ParcelFileDescriptor;
27import android.os.RemoteException;
28import android.util.Log;
29
30import java.io.File;
31import java.io.FileOutputStream;
32import java.io.IOException;
33import java.util.HashSet;
34import java.util.LinkedList;
35
36import libcore.io.ErrnoException;
37import libcore.io.Libcore;
38import libcore.io.OsConstants;
39import libcore.io.StructStat;
40
41/**
42 * Provides the central interface between an
43 * application and Android's data backup infrastructure.  An application that wishes
44 * to participate in the backup and restore mechanism will declare a subclass of
45 * {@link android.app.backup.BackupAgent}, implement the
46 * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()}
47 * and {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} methods,
48 * and provide the name of its backup agent class in its {@code AndroidManifest.xml} file via
49 * the <code><a
50 * href="{@docRoot}guide/topics/manifest/application-element.html">&lt;application&gt;</a></code>
51 * tag's {@code android:backupAgent} attribute.
52 * <h3>Basic Operation</h3>
53 * <p>
54 * When the application makes changes to data that it wishes to keep backed up,
55 * it should call the
56 * {@link android.app.backup.BackupManager#dataChanged() BackupManager.dataChanged()} method.
57 * This notifies the Android Backup Manager that the application needs an opportunity
58 * to update its backup image.  The Backup Manager, in turn, schedules a
59 * backup pass to be performed at an opportune time.
60 * <p>
61 * Restore operations are typically performed only when applications are first
62 * installed on a device.  At that time, the operating system checks to see whether
63 * there is a previously-saved data set available for the application being installed, and if so,
64 * begins an immediate restore pass to deliver the backup data as part of the installation
65 * process.
66 * <p>
67 * When a backup or restore pass is run, the application's process is launched
68 * (if not already running), the manifest-declared backup agent class (in the {@code
69 * android:backupAgent} attribute) is instantiated within
70 * that process, and the agent's {@link #onCreate()} method is invoked.  This prepares the
71 * agent instance to run the actual backup or restore logic.  At this point the
72 * agent's
73 * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()} or
74 * {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} method will be
75 * invoked as appropriate for the operation being performed.
76 * <p>
77 * A backup data set consists of one or more "entities," flattened binary data
78 * records that are each identified with a key string unique within the data set.  Adding a
79 * record to the active data set or updating an existing record is done by simply
80 * writing new entity data under the desired key.  Deleting an entity from the data set
81 * is done by writing an entity under that key with header specifying a negative data
82 * size, and no actual entity data.
83 * <p>
84 * <b>Helper Classes</b>
85 * <p>
86 * An extensible agent based on convenient helper classes is available in
87 * {@link android.app.backup.BackupAgentHelper}.  That class is particularly
88 * suited to handling of simple file or {@link android.content.SharedPreferences}
89 * backup and restore.
90 *
91 * @see android.app.backup.BackupManager
92 * @see android.app.backup.BackupAgentHelper
93 * @see android.app.backup.BackupDataInput
94 * @see android.app.backup.BackupDataOutput
95 */
96public abstract class BackupAgent extends ContextWrapper {
97    private static final String TAG = "BackupAgent";
98    private static final boolean DEBUG = true;
99
100    /** @hide */
101    public static final int TYPE_EOF = 0;
102
103    /**
104     * During a full restore, indicates that the file system object being restored
105     * is an ordinary file.
106     */
107    public static final int TYPE_FILE = 1;
108
109    /**
110     * During a full restore, indicates that the file system object being restored
111     * is a directory.
112     */
113    public static final int TYPE_DIRECTORY = 2;
114
115    /** @hide */
116    public static final int TYPE_SYMLINK = 3;
117
118    public BackupAgent() {
119        super(null);
120    }
121
122    /**
123     * Provided as a convenience for agent implementations that need an opportunity
124     * to do one-time initialization before the actual backup or restore operation
125     * is begun.
126     * <p>
127     * Agents do not need to override this method.
128     */
129    public void onCreate() {
130    }
131
132    /**
133     * Provided as a convenience for agent implementations that need to do some
134     * sort of shutdown process after backup or restore is completed.
135     * <p>
136     * Agents do not need to override this method.
137     */
138    public void onDestroy() {
139    }
140
141    /**
142     * The application is being asked to write any data changed since the last
143     * time it performed a backup operation. The state data recorded during the
144     * last backup pass is provided in the <code>oldState</code> file
145     * descriptor. If <code>oldState</code> is <code>null</code>, no old state
146     * is available and the application should perform a full backup. In both
147     * cases, a representation of the final backup state after this pass should
148     * be written to the file pointed to by the file descriptor wrapped in
149     * <code>newState</code>.
150     * <p>
151     * Each entity written to the {@link android.app.backup.BackupDataOutput}
152     * <code>data</code> stream will be transmitted
153     * over the current backup transport and stored in the remote data set under
154     * the key supplied as part of the entity.  Writing an entity with a negative
155     * data size instructs the transport to delete whatever entity currently exists
156     * under that key from the remote data set.
157     *
158     * @param oldState An open, read-only ParcelFileDescriptor pointing to the
159     *            last backup state provided by the application. May be
160     *            <code>null</code>, in which case no prior state is being
161     *            provided and the application should perform a full backup.
162     * @param data A structured wrapper around an open, read/write
163     *            file descriptor pointing to the backup data destination.
164     *            Typically the application will use backup helper classes to
165     *            write to this file.
166     * @param newState An open, read/write ParcelFileDescriptor pointing to an
167     *            empty file. The application should record the final backup
168     *            state here after writing the requested data to the <code>data</code>
169     *            output stream.
170     */
171    public abstract void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
172             ParcelFileDescriptor newState) throws IOException;
173
174    /**
175     * The application is being restored from backup and should replace any
176     * existing data with the contents of the backup. The backup data is
177     * provided through the <code>data</code> parameter. Once
178     * the restore is finished, the application should write a representation of
179     * the final state to the <code>newState</code> file descriptor.
180     * <p>
181     * The application is responsible for properly erasing its old data and
182     * replacing it with the data supplied to this method. No "clear user data"
183     * operation will be performed automatically by the operating system. The
184     * exception to this is in the case of a failed restore attempt: if
185     * onRestore() throws an exception, the OS will assume that the
186     * application's data may now be in an incoherent state, and will clear it
187     * before proceeding.
188     *
189     * @param data A structured wrapper around an open, read-only
190     *            file descriptor pointing to a full snapshot of the
191     *            application's data.  The application should consume every
192     *            entity represented in this data stream.
193     * @param appVersionCode The value of the <a
194     * href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code
195     *            android:versionCode}</a> manifest attribute,
196     *            from the application that backed up this particular data set. This
197     *            makes it possible for an application's agent to distinguish among any
198     *            possible older data versions when asked to perform the restore
199     *            operation.
200     * @param newState An open, read/write ParcelFileDescriptor pointing to an
201     *            empty file. The application should record the final backup
202     *            state here after restoring its data from the <code>data</code> stream.
203     *            When a full-backup dataset is being restored, this will be <code>null</code>.
204     */
205    public abstract void onRestore(BackupDataInput data, int appVersionCode,
206            ParcelFileDescriptor newState)
207            throws IOException;
208
209    /**
210     * The default implementation backs up the entirety of the application's "owned"
211     * file system trees to the output.
212     */
213    public void onFullBackup(FullBackupDataOutput data) throws IOException {
214        ApplicationInfo appInfo = getApplicationInfo();
215
216        String rootDir = new File(appInfo.dataDir).getCanonicalPath();
217        String filesDir = getFilesDir().getCanonicalPath();
218        String databaseDir = getDatabasePath("foo").getParentFile().getCanonicalPath();
219        String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
220        String cacheDir = getCacheDir().getCanonicalPath();
221        String libDir = (appInfo.nativeLibraryDir != null)
222                ? new File(appInfo.nativeLibraryDir).getCanonicalPath()
223                : null;
224
225        // Filters, the scan queue, and the set of resulting entities
226        HashSet<String> filterSet = new HashSet<String>();
227        String packageName = getPackageName();
228
229        // Okay, start with the app's root tree, but exclude all of the canonical subdirs
230        if (libDir != null) {
231            filterSet.add(libDir);
232        }
233        filterSet.add(cacheDir);
234        filterSet.add(databaseDir);
235        filterSet.add(sharedPrefsDir);
236        filterSet.add(filesDir);
237        fullBackupFileTree(packageName, FullBackup.ROOT_TREE_TOKEN, rootDir, filterSet, data);
238
239        // Now do the same for the files dir, db dir, and shared prefs dir
240        filterSet.add(rootDir);
241        filterSet.remove(filesDir);
242        fullBackupFileTree(packageName, FullBackup.DATA_TREE_TOKEN, filesDir, filterSet, data);
243
244        filterSet.add(filesDir);
245        filterSet.remove(databaseDir);
246        fullBackupFileTree(packageName, FullBackup.DATABASE_TREE_TOKEN, databaseDir, filterSet, data);
247
248        filterSet.add(databaseDir);
249        filterSet.remove(sharedPrefsDir);
250        fullBackupFileTree(packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, sharedPrefsDir, filterSet, data);
251    }
252
253    /**
254     * Write an entire file as part of a full-backup operation.  The file's contents
255     * will be delivered to the backup destination along with the metadata necessary
256     * to place it with the proper location and permissions on the device where the
257     * data is restored.
258     *
259     * @param file The file to be backed up.  The file must exist and be readable by
260     *     the caller.
261     * @param output The destination to which the backed-up file data will be sent.
262     */
263    public final void fullBackupFile(File file, FullBackupDataOutput output) {
264        // Look up where all of our various well-defined dir trees live on this device
265        String mainDir;
266        String filesDir;
267        String dbDir;
268        String spDir;
269        String cacheDir;
270        String libDir;
271        String filePath;
272
273        ApplicationInfo appInfo = getApplicationInfo();
274
275        try {
276            mainDir = new File(appInfo.dataDir).getCanonicalPath();
277            filesDir = getFilesDir().getCanonicalPath();
278            dbDir = getDatabasePath("foo").getParentFile().getCanonicalPath();
279            spDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
280            cacheDir = getCacheDir().getCanonicalPath();
281            libDir = (appInfo.nativeLibraryDir == null)
282                    ? null
283                    : new File(appInfo.nativeLibraryDir).getCanonicalPath();
284
285            // Now figure out which well-defined tree the file is placed in, working from
286            // most to least specific.  We also specifically exclude the lib and cache dirs.
287            filePath = file.getCanonicalPath();
288        } catch (IOException e) {
289            Log.w(TAG, "Unable to obtain canonical paths");
290            return;
291        }
292
293        if (filePath.startsWith(cacheDir) || filePath.startsWith(libDir)) {
294            Log.w(TAG, "lib and cache files are not backed up");
295            return;
296        }
297
298        final String domain;
299        String rootpath = null;
300        if (filePath.startsWith(dbDir)) {
301            domain = FullBackup.DATABASE_TREE_TOKEN;
302            rootpath = dbDir;
303        } else if (filePath.startsWith(spDir)) {
304            domain = FullBackup.SHAREDPREFS_TREE_TOKEN;
305            rootpath = spDir;
306        } else if (filePath.startsWith(filesDir)) {
307            domain = FullBackup.DATA_TREE_TOKEN;
308            rootpath = filesDir;
309        } else if (filePath.startsWith(mainDir)) {
310            domain = FullBackup.ROOT_TREE_TOKEN;
311            rootpath = mainDir;
312        } else {
313            Log.w(TAG, "File " + filePath + " is in an unsupported location; skipping");
314            return;
315        }
316
317        // And now that we know where it lives, semantically, back it up appropriately
318        Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain
319                + " rootpath=" + rootpath);
320        FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath,
321                output.getData());
322    }
323
324    /**
325     * Scan the dir tree (if it actually exists) and process each entry we find.  If the
326     * 'excludes' parameter is non-null, it is consulted each time a new file system entity
327     * is visited to see whether that entity (and its subtree, if appropriate) should be
328     * omitted from the backup process.
329     *
330     * @hide
331     */
332    protected final void fullBackupFileTree(String packageName, String domain, String rootPath,
333            HashSet<String> excludes, FullBackupDataOutput output) {
334        File rootFile = new File(rootPath);
335        if (rootFile.exists()) {
336            LinkedList<File> scanQueue = new LinkedList<File>();
337            scanQueue.add(rootFile);
338
339            while (scanQueue.size() > 0) {
340                File file = scanQueue.remove(0);
341                String filePath;
342                try {
343                    filePath = file.getCanonicalPath();
344
345                    // prune this subtree?
346                    if (excludes != null && excludes.contains(filePath)) {
347                        continue;
348                    }
349
350                    // If it's a directory, enqueue its contents for scanning.
351                    StructStat stat = Libcore.os.lstat(filePath);
352                    if (OsConstants.S_ISLNK(stat.st_mode)) {
353                        if (DEBUG) Log.i(TAG, "Symlink (skipping)!: " + file);
354                        continue;
355                    } else if (OsConstants.S_ISDIR(stat.st_mode)) {
356                        File[] contents = file.listFiles();
357                        if (contents != null) {
358                            for (File entry : contents) {
359                                scanQueue.add(0, entry);
360                            }
361                        }
362                    }
363                } catch (IOException e) {
364                    if (DEBUG) Log.w(TAG, "Error canonicalizing path of " + file);
365                    continue;
366                } catch (ErrnoException e) {
367                    if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e);
368                    continue;
369                }
370
371                // Finally, back this file up before proceeding
372                FullBackup.backupToTar(packageName, domain, null, rootPath, filePath,
373                        output.getData());
374            }
375        }
376    }
377
378    /**
379     * Handle the data delivered via the given file descriptor during a full restore
380     * operation.  The agent is given the path to the file's original location as well
381     * as its size and metadata.
382     * <p>
383     * The file descriptor can only be read for {@code size} bytes; attempting to read
384     * more data has undefined behavior.
385     * <p>
386     * The default implementation creates the destination file/directory and populates it
387     * with the data from the file descriptor, then sets the file's access mode and
388     * modification time to match the restore arguments.
389     *
390     * @param data A read-only file descriptor from which the agent can read {@code size}
391     *     bytes of file data.
392     * @param size The number of bytes of file content to be restored to the given
393     *     destination.  If the file system object being restored is a directory, {@code size}
394     *     will be zero.
395     * @param destination The File on disk to be restored with the given data.
396     * @param type The kind of file system object being restored.  This will be either
397     *     {@link BackupAgent#TYPE_FILE} or {@link BackupAgent#TYPE_DIRECTORY}.
398     * @param mode The access mode to be assigned to the destination after its data is
399     *     written.  This is in the standard format used by {@code chmod()}.
400     * @param mtime The modification time of the file when it was backed up, suitable to
401     *     be assigned to the file after its data is written.
402     * @throws IOException
403     */
404    public void onRestoreFile(ParcelFileDescriptor data, long size,
405            File destination, int type, long mode, long mtime)
406            throws IOException {
407        FullBackup.restoreFile(data, size, type, mode, mtime, destination);
408    }
409
410    /**
411     * Only specialized platform agents should overload this entry point to support
412     * restores to crazy non-app locations.
413     * @hide
414     */
415    protected void onRestoreFile(ParcelFileDescriptor data, long size,
416            int type, String domain, String path, long mode, long mtime)
417            throws IOException {
418        String basePath = null;
419
420        if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type
421                + " domain=" + domain + " relpath=" + path + " mode=" + mode
422                + " mtime=" + mtime);
423
424        // Parse out the semantic domains into the correct physical location
425        if (domain.equals(FullBackup.DATA_TREE_TOKEN)) {
426            basePath = getFilesDir().getCanonicalPath();
427        } else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) {
428            basePath = getDatabasePath("foo").getParentFile().getCanonicalPath();
429        } else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) {
430            basePath = new File(getApplicationInfo().dataDir).getCanonicalPath();
431        } else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
432            basePath = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
433        } else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) {
434            basePath = getCacheDir().getCanonicalPath();
435        } else {
436            // Not a supported location
437            Log.i(TAG, "Data restored from non-app domain " + domain + ", ignoring");
438        }
439
440        // Now that we've figured out where the data goes, send it on its way
441        if (basePath != null) {
442            File outFile = new File(basePath, path);
443            if (DEBUG) Log.i(TAG, "[" + domain + " : " + path + "] mapped to " + outFile.getPath());
444            onRestoreFile(data, size, outFile, type, mode, mtime);
445        } else {
446            // Not a supported output location?  We need to consume the data
447            // anyway, so just use the default "copy the data out" implementation
448            // with a null destination.
449            if (DEBUG) Log.i(TAG, "[ skipping data from unsupported domain " + domain + "]");
450            FullBackup.restoreFile(data, size, type, mode, mtime, null);
451        }
452    }
453
454    // ----- Core implementation -----
455
456    /** @hide */
457    public final IBinder onBind() {
458        return mBinder;
459    }
460
461    private final IBinder mBinder = new BackupServiceBinder().asBinder();
462
463    /** @hide */
464    public void attach(Context context) {
465        attachBaseContext(context);
466    }
467
468    // ----- IBackupService binder interface -----
469    private class BackupServiceBinder extends IBackupAgent.Stub {
470        private static final String TAG = "BackupServiceBinder";
471
472        @Override
473        public void doBackup(ParcelFileDescriptor oldState,
474                ParcelFileDescriptor data,
475                ParcelFileDescriptor newState,
476                int token, IBackupManager callbackBinder) throws RemoteException {
477            // Ensure that we're running with the app's normal permission level
478            long ident = Binder.clearCallingIdentity();
479
480            if (DEBUG) Log.v(TAG, "doBackup() invoked");
481            BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor());
482
483            try {
484                BackupAgent.this.onBackup(oldState, output, newState);
485            } catch (IOException ex) {
486                Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
487                throw new RuntimeException(ex);
488            } catch (RuntimeException ex) {
489                Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
490                throw ex;
491            } finally {
492                Binder.restoreCallingIdentity(ident);
493                try {
494                    callbackBinder.opComplete(token);
495                } catch (RemoteException e) {
496                    // we'll time out anyway, so we're safe
497                }
498            }
499        }
500
501        @Override
502        public void doRestore(ParcelFileDescriptor data, int appVersionCode,
503                ParcelFileDescriptor newState,
504                int token, IBackupManager callbackBinder) throws RemoteException {
505            // Ensure that we're running with the app's normal permission level
506            long ident = Binder.clearCallingIdentity();
507
508            if (DEBUG) Log.v(TAG, "doRestore() invoked");
509            BackupDataInput input = new BackupDataInput(data.getFileDescriptor());
510            try {
511                BackupAgent.this.onRestore(input, appVersionCode, newState);
512            } catch (IOException ex) {
513                Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex);
514                throw new RuntimeException(ex);
515            } catch (RuntimeException ex) {
516                Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex);
517                throw ex;
518            } finally {
519                Binder.restoreCallingIdentity(ident);
520                try {
521                    callbackBinder.opComplete(token);
522                } catch (RemoteException e) {
523                    // we'll time out anyway, so we're safe
524                }
525            }
526        }
527
528        @Override
529        public void doFullBackup(ParcelFileDescriptor data,
530                int token, IBackupManager callbackBinder) {
531            // Ensure that we're running with the app's normal permission level
532            long ident = Binder.clearCallingIdentity();
533
534            if (DEBUG) Log.v(TAG, "doFullBackup() invoked");
535
536            try {
537                BackupAgent.this.onFullBackup(new FullBackupDataOutput(data));
538            } catch (IOException ex) {
539                Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
540                throw new RuntimeException(ex);
541            } catch (RuntimeException ex) {
542                Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
543                throw ex;
544            } finally {
545                // Send the EOD marker indicating that there is no more data
546                // forthcoming from this agent.
547                try {
548                    FileOutputStream out = new FileOutputStream(data.getFileDescriptor());
549                    byte[] buf = new byte[4];
550                    out.write(buf);
551                } catch (IOException e) {
552                    Log.e(TAG, "Unable to finalize backup stream!");
553                }
554
555                Binder.restoreCallingIdentity(ident);
556                try {
557                    callbackBinder.opComplete(token);
558                } catch (RemoteException e) {
559                    // we'll time out anyway, so we're safe
560                }
561            }
562        }
563
564        @Override
565        public void doRestoreFile(ParcelFileDescriptor data, long size,
566                int type, String domain, String path, long mode, long mtime,
567                int token, IBackupManager callbackBinder) throws RemoteException {
568            long ident = Binder.clearCallingIdentity();
569            try {
570                BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime);
571            } catch (IOException e) {
572                throw new RuntimeException(e);
573            } finally {
574                Binder.restoreCallingIdentity(ident);
575                try {
576                    callbackBinder.opComplete(token);
577                } catch (RemoteException e) {
578                    // we'll time out anyway, so we're safe
579                }
580            }
581        }
582    }
583}
584