BackupAgent.java revision 7926a693c4a4f4d2a2d352343bca23e189c7420d
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).getAbsolutePath();
217        String filesDir = getFilesDir().getAbsolutePath();
218        String databaseDir = getDatabasePath("foo").getParentFile().getAbsolutePath();
219        String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getAbsolutePath();
220        String cacheDir = getCacheDir().getAbsolutePath();
221        String libDir = (appInfo.nativeLibraryDir != null)
222                ? new File(appInfo.nativeLibraryDir).getAbsolutePath()
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     * @hide
259     *
260     * @param context The BackupAgent that is calling this method.  It is an error to
261     *     call it from something other than a running BackupAgent instance.
262     * @param file The file to be backed up.  The file must exist and be readable by
263     *     the caller.
264     * @param output The destination to which the backed-up file data will be sent.
265     */
266    public final void fullBackupFile(File file, FullBackupDataOutput output) {
267        // Look up where all of our various well-defined dir trees live on this device
268        String mainDir;
269        String filesDir;
270        String dbDir;
271        String spDir;
272        String cacheDir;
273        String libDir;
274
275        ApplicationInfo appInfo = getApplicationInfo();
276
277        mainDir = new File(appInfo.dataDir).getAbsolutePath();
278        filesDir = getFilesDir().getAbsolutePath();
279        dbDir = getDatabasePath("foo").getParentFile().getAbsolutePath();
280        spDir = getSharedPrefsFile("foo").getParentFile().getAbsolutePath();
281        cacheDir = getCacheDir().getAbsolutePath();
282        libDir = (appInfo.nativeLibraryDir == null) ? null
283                : new File(appInfo.nativeLibraryDir).getAbsolutePath();
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        String filePath = file.getAbsolutePath();
288
289        if (filePath.startsWith(cacheDir) || filePath.startsWith(libDir)) {
290            Log.w(TAG, "lib and cache files are not backed up");
291            return;
292        }
293
294        final String domain;
295        String rootpath = null;
296        if (filePath.startsWith(dbDir)) {
297            domain = FullBackup.DATABASE_TREE_TOKEN;
298            rootpath = dbDir;
299        } else if (filePath.startsWith(spDir)) {
300            domain = FullBackup.SHAREDPREFS_TREE_TOKEN;
301            rootpath = spDir;
302        } else if (filePath.startsWith(filesDir)) {
303            domain = FullBackup.DATA_TREE_TOKEN;
304            rootpath = filesDir;
305        } else if (filePath.startsWith(mainDir)) {
306            domain = FullBackup.ROOT_TREE_TOKEN;
307            rootpath = mainDir;
308        } else {
309            Log.w(TAG, "File " + filePath + " is in an unsupported location; skipping");
310            return;
311        }
312
313        // And now that we know where it lives, semantically, back it up appropriately
314        Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain
315                + " rootpath=" + rootpath);
316        FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath,
317                output.getData());
318    }
319
320    /**
321     * Scan the dir tree (if it actually exists) and process each entry we find.  If the
322     * 'excludes' parameter is non-null, it is consulted each time a new file system entity
323     * is visited to see whether that entity (and its subtree, if appropriate) should be
324     * omitted from the backup process.
325     *
326     * @hide
327     */
328    protected final void fullBackupFileTree(String packageName, String domain, String rootPath,
329            HashSet<String> excludes, FullBackupDataOutput output) {
330        File rootFile = new File(rootPath);
331        if (rootFile.exists()) {
332            LinkedList<File> scanQueue = new LinkedList<File>();
333            scanQueue.add(rootFile);
334
335            while (scanQueue.size() > 0) {
336                File file = scanQueue.remove(0);
337                String filePath = file.getAbsolutePath();
338
339                // prune this subtree?
340                if (excludes != null && excludes.contains(filePath)) {
341                    continue;
342                }
343
344                // If it's a directory, enqueue its contents for scanning.
345                try {
346                    StructStat stat = Libcore.os.lstat(filePath);
347                    if (OsConstants.S_ISLNK(stat.st_mode)) {
348                        if (DEBUG) Log.i(TAG, "Symlink (skipping)!: " + file);
349                        continue;
350                    } else if (OsConstants.S_ISDIR(stat.st_mode)) {
351                        File[] contents = file.listFiles();
352                        if (contents != null) {
353                            for (File entry : contents) {
354                                scanQueue.add(0, entry);
355                            }
356                        }
357                    }
358                } catch (ErrnoException e) {
359                    if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e);
360                    continue;
361                }
362
363                // Finally, back this file up before proceeding
364                FullBackup.backupToTar(packageName, domain, null, rootPath, filePath,
365                        output.getData());
366            }
367        }
368    }
369
370    /**
371     * Handle the data delivered via the given file descriptor during a full restore
372     * operation.  The agent is given the path to the file's original location as well
373     * as its size and metadata.
374     * <p>
375     * The file descriptor can only be read for {@code size} bytes; attempting to read
376     * more data has undefined behavior.
377     * <p>
378     * The default implementation creates the destination file/directory and populates it
379     * with the data from the file descriptor, then sets the file's access mode and
380     * modification time to match the restore arguments.
381     *
382     * @param data A read-only file descriptor from which the agent can read {@code size}
383     *     bytes of file data.
384     * @param size The number of bytes of file content to be restored to the given
385     *     destination.  If the file system object being restored is a directory, {@code size}
386     *     will be zero.
387     * @param destination The File on disk to be restored with the given data.
388     * @param type The kind of file system object being restored.  This will be either
389     *     {@link BackupAgent#TYPE_FILE} or {@link BackupAgent#TYPE_DIRECTORY}.
390     * @param mode The access mode to be assigned to the destination after its data is
391     *     written.  This is in the standard format used by {@code chmod()}.
392     * @param mtime The modification time of the file when it was backed up, suitable to
393     *     be assigned to the file after its data is written.
394     * @throws IOException
395     */
396    public void onRestoreFile(ParcelFileDescriptor data, long size,
397            File destination, int type, long mode, long mtime)
398            throws IOException {
399        FullBackup.restoreFile(data, size, type, mode, mtime, destination);
400    }
401
402    /**
403     * Only specialized platform agents should overload this entry point to support
404     * restores to crazy non-app locations.
405     * @hide
406     */
407    protected void onRestoreFile(ParcelFileDescriptor data, long size,
408            int type, String domain, String path, long mode, long mtime)
409            throws IOException {
410        String basePath = null;
411
412        if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type
413                + " domain=" + domain + " relpath=" + path + " mode=" + mode
414                + " mtime=" + mtime);
415
416        // Parse out the semantic domains into the correct physical location
417        if (domain.equals(FullBackup.DATA_TREE_TOKEN)) {
418            basePath = getFilesDir().getAbsolutePath();
419        } else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) {
420            basePath = getDatabasePath("foo").getParentFile().getAbsolutePath();
421        } else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) {
422            basePath = new File(getApplicationInfo().dataDir).getAbsolutePath();
423        } else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
424            basePath = getSharedPrefsFile("foo").getParentFile().getAbsolutePath();
425        } else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) {
426            basePath = getCacheDir().getAbsolutePath();
427        } else {
428            // Not a supported location
429            Log.i(TAG, "Data restored from non-app domain " + domain + ", ignoring");
430        }
431
432        // Now that we've figured out where the data goes, send it on its way
433        if (basePath != null) {
434            File outFile = new File(basePath, path);
435            if (DEBUG) Log.i(TAG, "[" + domain + " : " + path + "] mapped to " + outFile.getPath());
436            onRestoreFile(data, size, outFile, type, mode, mtime);
437        } else {
438            // Not a supported output location?  We need to consume the data
439            // anyway, so just use the default "copy the data out" implementation
440            // with a null destination.
441            if (DEBUG) Log.i(TAG, "[ skipping data from unsupported domain " + domain + "]");
442            FullBackup.restoreFile(data, size, type, mode, mtime, null);
443        }
444    }
445
446    // ----- Core implementation -----
447
448    /** @hide */
449    public final IBinder onBind() {
450        return mBinder;
451    }
452
453    private final IBinder mBinder = new BackupServiceBinder().asBinder();
454
455    /** @hide */
456    public void attach(Context context) {
457        attachBaseContext(context);
458    }
459
460    // ----- IBackupService binder interface -----
461    private class BackupServiceBinder extends IBackupAgent.Stub {
462        private static final String TAG = "BackupServiceBinder";
463
464        @Override
465        public void doBackup(ParcelFileDescriptor oldState,
466                ParcelFileDescriptor data,
467                ParcelFileDescriptor newState,
468                int token, IBackupManager callbackBinder) throws RemoteException {
469            // Ensure that we're running with the app's normal permission level
470            long ident = Binder.clearCallingIdentity();
471
472            if (DEBUG) Log.v(TAG, "doBackup() invoked");
473            BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor());
474
475            try {
476                BackupAgent.this.onBackup(oldState, output, newState);
477            } catch (IOException ex) {
478                Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
479                throw new RuntimeException(ex);
480            } catch (RuntimeException ex) {
481                Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
482                throw ex;
483            } finally {
484                Binder.restoreCallingIdentity(ident);
485                try {
486                    callbackBinder.opComplete(token);
487                } catch (RemoteException e) {
488                    // we'll time out anyway, so we're safe
489                }
490            }
491        }
492
493        @Override
494        public void doRestore(ParcelFileDescriptor data, int appVersionCode,
495                ParcelFileDescriptor newState,
496                int token, IBackupManager callbackBinder) throws RemoteException {
497            // Ensure that we're running with the app's normal permission level
498            long ident = Binder.clearCallingIdentity();
499
500            if (DEBUG) Log.v(TAG, "doRestore() invoked");
501            BackupDataInput input = new BackupDataInput(data.getFileDescriptor());
502            try {
503                BackupAgent.this.onRestore(input, appVersionCode, newState);
504            } catch (IOException ex) {
505                Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex);
506                throw new RuntimeException(ex);
507            } catch (RuntimeException ex) {
508                Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex);
509                throw ex;
510            } finally {
511                Binder.restoreCallingIdentity(ident);
512                try {
513                    callbackBinder.opComplete(token);
514                } catch (RemoteException e) {
515                    // we'll time out anyway, so we're safe
516                }
517            }
518        }
519
520        @Override
521        public void doFullBackup(ParcelFileDescriptor data,
522                int token, IBackupManager callbackBinder) {
523            // Ensure that we're running with the app's normal permission level
524            long ident = Binder.clearCallingIdentity();
525
526            if (DEBUG) Log.v(TAG, "doFullBackup() invoked");
527
528            try {
529                BackupAgent.this.onFullBackup(new FullBackupDataOutput(data));
530            } catch (IOException ex) {
531                Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
532                throw new RuntimeException(ex);
533            } catch (RuntimeException ex) {
534                Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
535                throw ex;
536            } finally {
537                // Send the EOD marker indicating that there is no more data
538                // forthcoming from this agent.
539                try {
540                    FileOutputStream out = new FileOutputStream(data.getFileDescriptor());
541                    byte[] buf = new byte[4];
542                    out.write(buf);
543                } catch (IOException e) {
544                    Log.e(TAG, "Unable to finalize backup stream!");
545                }
546
547                Binder.restoreCallingIdentity(ident);
548                try {
549                    callbackBinder.opComplete(token);
550                } catch (RemoteException e) {
551                    // we'll time out anyway, so we're safe
552                }
553            }
554        }
555
556        @Override
557        public void doRestoreFile(ParcelFileDescriptor data, long size,
558                int type, String domain, String path, long mode, long mtime,
559                int token, IBackupManager callbackBinder) throws RemoteException {
560            long ident = Binder.clearCallingIdentity();
561            try {
562                BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime);
563            } catch (IOException e) {
564                throw new RuntimeException(e);
565            } finally {
566                Binder.restoreCallingIdentity(ident);
567                try {
568                    callbackBinder.opComplete(token);
569                } catch (RemoteException e) {
570                    // we'll time out anyway, so we're safe
571                }
572            }
573        }
574    }
575}
576