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