Reset backup tracking in response to transport data-wipe notification

When attempting a backup, the transport may inform us that the backend is in an
uninitialized state.  This typically means that the device's data has been wiped
after a period [e.g. 90 days] of inactivity.  This means that we need to
re-store all data subject to backup, and all of our incremental state tracking
on the device is now stale.

In response, we wipe all of our recorded backup state and restart the backup
pass on all participants.
diff --git a/core/java/com/android/internal/backup/BackupConstants.java b/core/java/com/android/internal/backup/BackupConstants.java
new file mode 100644
index 0000000..3ee11bd
--- /dev/null
+++ b/core/java/com/android/internal/backup/BackupConstants.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.backup;
+
+/**
+ * Constants used internally between the backup manager and its transports
+ */
+public class BackupConstants {
+    public static final int TRANSPORT_OK = 0;
+    public static final int TRANSPORT_ERROR = 1;
+    public static final int TRANSPORT_NOT_INITIALIZED = 2;
+}
diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl
index 250bc91..47496a9 100644
--- a/core/java/com/android/internal/backup/IBackupTransport.aidl
+++ b/core/java/com/android/internal/backup/IBackupTransport.aidl
@@ -81,10 +81,14 @@
      *   will be erased prior to the storage of the data provided here.  The purpose of this
      *   is to provide a guarantee that no stale data exists in the restore set when the
      *   device begins providing backups.
-     * @return false if errors occurred (the backup should be aborted and rescheduled),
-     *   true if everything is OK so far (but {@link #finishBackup} must be called).
+     * @return If everything is okay so far, returns zero (but {@link #finishBackup} must
+     *   still be called).  If the backend dataset has unexpectedly become unavailable,
+     *   such as when it is deleted after a period of device inactivity, returns {@link
+     *   BackupManager#DATASET_UNAVAILABLE}; in this case, the transport should be
+     *   reinitalized and the entire backup pass restarted.  Any other nonzero value is a
+     *   fatal error requiring that this package's backup be aborted and rescheduled.
      */
-    boolean performBackup(in PackageInfo packageInfo, in ParcelFileDescriptor inFd,
+    int performBackup(in PackageInfo packageInfo, in ParcelFileDescriptor inFd,
             boolean wipeAllFirst);
 
     /**
diff --git a/core/java/com/android/internal/backup/LocalTransport.java b/core/java/com/android/internal/backup/LocalTransport.java
index 981ea82..603f691f 100644
--- a/core/java/com/android/internal/backup/LocalTransport.java
+++ b/core/java/com/android/internal/backup/LocalTransport.java
@@ -56,7 +56,7 @@
         return 0;
     }
 
-    public boolean performBackup(PackageInfo packageInfo, ParcelFileDescriptor data,
+    public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data,
             boolean wipeAllFirst) throws RemoteException {
         if (DEBUG) Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName);
 
@@ -99,7 +99,7 @@
                         entity.write(buf, 0, dataSize);
                     } catch (IOException e) {
                         Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath());
-                        return false;
+                        return BackupConstants.TRANSPORT_ERROR;
                     } finally {
                         entity.close();
                     }
@@ -107,11 +107,11 @@
                     entityFile.delete();
                 }
             }
-            return true;
+            return BackupConstants.TRANSPORT_OK;
         } catch (IOException e) {
             // oops, something went wrong.  abort the operation and return error.
             Log.v(TAG, "Exception reading backup input:", e);
-            return false;
+            return BackupConstants.TRANSPORT_ERROR;
         }
     }
 
diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java
index 854a6f9..861f66d 100644
--- a/services/java/com/android/server/BackupManagerService.java
+++ b/services/java/com/android/server/BackupManagerService.java
@@ -57,6 +57,7 @@
 import android.backup.IRestoreSession;
 import android.backup.RestoreSet;
 
+import com.android.internal.backup.BackupConstants;
 import com.android.internal.backup.LocalTransport;
 import com.android.internal.backup.IBackupTransport;
 
@@ -101,6 +102,7 @@
     private static final int BACKUP_AGENT_FAILURE_EVENT = 2823;
     private static final int BACKUP_PACKAGE_EVENT = 2824;
     private static final int BACKUP_SUCCESS_EVENT = 2825;
+    private static final int BACKUP_RESET_EVENT = 2826;
 
     private static final int RESTORE_START_EVENT = 2830;
     private static final int RESTORE_TRANSPORT_FAILURE_EVENT = 2831;
@@ -406,6 +408,47 @@
         }
     }
 
+    // Reset all of our bookkeeping, in response to having been told that
+    // the backend data has been wiped [due to idle expiry, for example],
+    // so we must re-upload all saved settings.
+    void resetBackupState(File stateFileDir) {
+        synchronized (mQueueLock) {
+            // Wipe the "what we've ever backed up" tracking
+            try {
+                // close the ever-stored journal...
+                if (mEverStoredStream != null) {
+                    mEverStoredStream.close();
+                }
+                // ... so we can delete it and start over
+                mEverStored.delete();
+                mEverStoredStream = new RandomAccessFile(mEverStored, "rwd");
+            } catch (IOException e) {
+                Log.e(TAG, "Unable to open known-stored file!");
+                mEverStoredStream = null;
+            }
+            mEverStoredApps.clear();
+
+            // Remove all the state files
+            for (File sf : stateFileDir.listFiles()) {
+                sf.delete();
+            }
+
+            // Enqueue a new backup of every participant
+            int N = mBackupParticipants.size();
+            for (int i=0; i<N; i++) {
+                int uid = mBackupParticipants.keyAt(i);
+                HashSet<ApplicationInfo> participants = mBackupParticipants.valueAt(i);
+                for (ApplicationInfo app: participants) {
+                    try {
+                        dataChanged(app.packageName);
+                    } catch (RemoteException e) {
+                        // can't happen; we're in the same process
+                    }
+                }
+            }
+        }
+    }
+
     // Add a transport to our set of available backends
     private void registerTransport(String name, IBackupTransport transport) {
         synchronized (mTransports) {
@@ -891,8 +934,22 @@
                 // If we haven't stored anything yet, we need to do an init
                 // operation along with recording the metadata blob.
                 boolean needInit = (mEverStoredApps.size() == 0);
-                processOneBackup(pmRequest, IBackupAgent.Stub.asInterface(pmAgent.onBind()),
+                int result = processOneBackup(pmRequest,
+                        IBackupAgent.Stub.asInterface(pmAgent.onBind()),
                         mTransport, needInit);
+                if (result == BackupConstants.TRANSPORT_NOT_INITIALIZED) {
+                    // The backend reports that our dataset has been wiped.  We need to
+                    // reset all of our bookkeeping and instead run a new backup pass for
+                    // everything.
+                    EventLog.writeEvent(BACKUP_RESET_EVENT, mTransport.transportDirName());
+                    resetBackupState(mStateDir);
+                    backupNow();
+                    return;
+                } else if (result != BackupConstants.TRANSPORT_OK) {
+                    // Give up if we couldn't even process the metadata
+                    Log.e(TAG, "Meta backup err " + result);
+                    return;
+                }
 
                 // Now run all the backups in our queue
                 int count = mQueue.size();
@@ -953,7 +1010,7 @@
             }
         }
 
-        void processOneBackup(BackupRequest request, IBackupAgent agent,
+        int processOneBackup(BackupRequest request, IBackupAgent agent,
                 IBackupTransport transport, boolean doInit) {
             final String packageName = request.appInfo.packageName;
             if (DEBUG) Log.d(TAG, "processOneBackup doBackup(" + doInit + ") on " + packageName);
@@ -1007,7 +1064,7 @@
                 EventLog.writeEvent(BACKUP_AGENT_FAILURE_EVENT, packageName, e.toString());
                 backupDataName.delete();
                 newStateName.delete();
-                return;
+                return BackupConstants.TRANSPORT_ERROR;
             } finally {
                 try { if (savedState != null) savedState.close(); } catch (IOException e) {}
                 try { if (backupData != null) backupData.close(); } catch (IOException e) {}
@@ -1027,7 +1084,13 @@
                     // hold off on finishBackup() until the end, which implies holding off on
                     // renaming *all* the output state files (see below) until that happens.
 
-                    if (!transport.performBackup(packInfo, backupData, doInit) ||
+                    int performOkay = transport.performBackup(packInfo, backupData, doInit);
+                    if (performOkay == BackupConstants.TRANSPORT_NOT_INITIALIZED) {
+                        Log.i(TAG, "Backend not initialized");
+                        return performOkay;
+                    }
+
+                    if ((performOkay != 0) ||
                         !transport.finishBackup()) {
                         throw new Exception("Backup transport failed");
                     }
@@ -1044,10 +1107,12 @@
             } catch (Exception e) {
                 Log.e(TAG, "Transport error backing up " + packageName, e);
                 EventLog.writeEvent(BACKUP_TRANSPORT_FAILURE_EVENT, packageName);
-                return;
+                return BackupConstants.TRANSPORT_ERROR;
             } finally {
                 try { if (backupData != null) backupData.close(); } catch (IOException e) {}
             }
+
+            return BackupConstants.TRANSPORT_OK;
         }
     }
 
@@ -1590,7 +1655,6 @@
         if (DEBUG) Log.v(TAG, "Scheduling immediate backup pass");
         synchronized (mQueueLock) {
             try {
-                if (DEBUG) Log.v(TAG, "sending immediate backup broadcast");
                 mRunBackupIntent.send();
             } catch (PendingIntent.CanceledException e) {
                 // should never happen