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