[SettingsState] fix boot loop due to corrupted database file

Use an explicit fallback file for each settings file. If the original file
is corrupted and fails to be parsed, use the fallback file instead.

The fallback files are updated once daily if the original files are
changed.

BUG: 119723324
Test: manual by pushing a malformed xml file
Change-Id: I1deb203cd1f53649a5675ad90290075fce6ca942
diff --git a/packages/SettingsProvider/AndroidManifest.xml b/packages/SettingsProvider/AndroidManifest.xml
index 839899e..04d3f94 100644
--- a/packages/SettingsProvider/AndroidManifest.xml
+++ b/packages/SettingsProvider/AndroidManifest.xml
@@ -21,5 +21,10 @@
                   android:singleUser="true"
                   android:initOrder="100"
                   android:visibleToInstantApps="true" />
+
+        <service
+            android:name="WriteFallbackSettingsFilesJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE"
+            android:exported="true"/>
     </application>
 </manifest>
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index d3d04e5..cb0fcc2 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -23,12 +23,16 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_MAGNIFICATION_CONTROLLER;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON_OVERLAY;
 
+import static com.android.providers.settings.SettingsState.FALLBACK_FILE_SUFFIX;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.app.backup.BackupManager;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentProvider;
@@ -55,12 +59,14 @@
 import android.os.Bundle;
 import android.os.DropBoxManager;
 import android.os.Environment;
+import android.os.FileUtils;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IUserRestrictionsListener;
 import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
@@ -95,6 +101,7 @@
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.ByteBuffer;
 import java.security.InvalidKeyException;
@@ -154,9 +161,11 @@
 
     private static final String LOG_TAG = "SettingsProvider";
 
-    private static final String TABLE_SYSTEM = "system";
-    private static final String TABLE_SECURE = "secure";
-    private static final String TABLE_GLOBAL = "global";
+    public static final String TABLE_SYSTEM = "system";
+    public static final String TABLE_SECURE = "secure";
+    public static final String TABLE_GLOBAL = "global";
+    public static final String TABLE_SSAID = "ssaid";
+    public static final String TABLE_CONFIG = "config";
 
     // Old tables no longer exist.
     private static final String TABLE_FAVORITES = "favorites";
@@ -205,6 +214,10 @@
     public static final String RESULT_ROWS_DELETED = "result_rows_deleted";
     public static final String RESULT_SETTINGS_LIST = "result_settings_list";
 
+    // Used for scheduling jobs to make a copy for the settings files
+    public static final int WRITE_FALLBACK_SETTINGS_FILES_JOB_ID = 1;
+    public static final long ONE_DAY_INTERVAL_MILLIS = 24 * 60 * 60 * 1000L;
+
     // Overlay specified settings whitelisted for Instant Apps
     private static final Set<String> OVERLAY_ALLOWED_GLOBAL_INSTANT_APP_SETTINGS = new ArraySet<>();
     private static final Set<String> OVERLAY_ALLOWED_SYSTEM_INSTANT_APP_SETTINGS = new ArraySet<>();
@@ -2484,6 +2497,68 @@
         }
     }
 
+    /**
+     * Schedule the job service to make a copy of all the settings files.
+     */
+    public void scheduleWriteFallbackFilesJob() {
+        final Context context = getContext();
+        final JobScheduler jobScheduler =
+                (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        if (jobScheduler == null) {
+            // Might happen: SettingsProvider is created before JobSchedulerService in system server
+            return;
+        }
+        // Check if the job is already scheduled. If so, skip scheduling another one
+        if (jobScheduler.getPendingJob(WRITE_FALLBACK_SETTINGS_FILES_JOB_ID) != null) {
+            return;
+        }
+        // Back up all settings files
+        final PersistableBundle bundle = new PersistableBundle();
+        final File globalSettingsFile = mSettingsRegistry.getSettingsFile(
+                makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM));
+        final File systemSettingsFile = mSettingsRegistry.getSettingsFile(
+                makeKey(SETTINGS_TYPE_SYSTEM, UserHandle.USER_SYSTEM));
+        final File secureSettingsFile = mSettingsRegistry.getSettingsFile(
+                makeKey(SETTINGS_TYPE_SECURE, UserHandle.USER_SYSTEM));
+        final File ssaidSettingsFile = mSettingsRegistry.getSettingsFile(
+                makeKey(SETTINGS_TYPE_SSAID, UserHandle.USER_SYSTEM));
+        final File configSettingsFile = mSettingsRegistry.getSettingsFile(
+                makeKey(SETTINGS_TYPE_CONFIG, UserHandle.USER_SYSTEM));
+        bundle.putString(TABLE_GLOBAL, globalSettingsFile.getAbsolutePath());
+        bundle.putString(TABLE_SYSTEM, systemSettingsFile.getAbsolutePath());
+        bundle.putString(TABLE_SECURE, secureSettingsFile.getAbsolutePath());
+        bundle.putString(TABLE_SSAID, ssaidSettingsFile.getAbsolutePath());
+        bundle.putString(TABLE_CONFIG, configSettingsFile.getAbsolutePath());
+        // Schedule the job to write the fallback files, once daily when phone is charging
+        jobScheduler.schedule(new JobInfo.Builder(WRITE_FALLBACK_SETTINGS_FILES_JOB_ID,
+                new ComponentName(context, WriteFallbackSettingsFilesJobService.class))
+                .setExtras(bundle)
+                .setPeriodic(ONE_DAY_INTERVAL_MILLIS)
+                .setRequiresCharging(true)
+                .setPersisted(true)
+                .build());
+    }
+
+    /**
+     * For each file in the given list, if it exists, copy it to a back up file. Ignore failures.
+     * @param filePaths List of paths of files that need to be backed up
+     */
+    public static void writeFallBackSettingsFiles(List<String> filePaths) {
+        final int numFiles = filePaths.size();
+        for (int i = 0; i < numFiles; i++) {
+            final String filePath = filePaths.get(i);
+            final File originalFile = new File(filePath);
+            if (SettingsState.stateFileExists(originalFile)) {
+                final File fallBackFile = new File(filePath + FALLBACK_FILE_SUFFIX);
+                try {
+                    FileUtils.copy(originalFile, fallBackFile);
+                } catch (IOException ex) {
+                    Slog.w(LOG_TAG, "Failed to write fallback file for: " + filePath);
+                }
+            }
+        }
+    }
+
     final class SettingsRegistry {
         private static final String DROPBOX_TAG_USERLOG = "restricted_profile_ssaid";
 
@@ -3431,6 +3506,7 @@
 
                     case MSG_NOTIFY_DATA_CHANGED: {
                         mBackupManager.dataChanged();
+                        scheduleWriteFallbackFilesJob();
                     } break;
                 }
             }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 6b8219e..6678cf6 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -27,6 +27,7 @@
 import android.content.pm.Signature;
 import android.os.Binder;
 import android.os.Build;
+import android.os.FileUtils;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -101,6 +102,8 @@
 
     public static final int VERSION_UNDEFINED = -1;
 
+    public static final String FALLBACK_FILE_SUFFIX = ".fallback";
+
     private static final String TAG_SETTINGS = "settings";
     private static final String TAG_SETTING = "setting";
     private static final String ATTR_PACKAGE = "package";
@@ -266,7 +269,7 @@
     public SettingsState(Context context, Object lock, File file, int key,
             int maxBytesPerAppPackage, Looper looper) {
         // It is important that we use the same lock as the settings provider
-        // to ensure multiple mutations on this state are atomicaly persisted
+        // to ensure multiple mutations on this state are atomically persisted
         // as the async persistence should be blocked while we make changes.
         mContext = context;
         mLock = lock;
@@ -998,24 +1001,56 @@
     }
 
     @GuardedBy("mLock")
-    private void readStateSyncLocked() {
+    private void readStateSyncLocked() throws IllegalStateException {
         FileInputStream in;
+        AtomicFile file = new AtomicFile(mStatePersistFile);
         try {
-            in = new AtomicFile(mStatePersistFile).openRead();
+            in = file.openRead();
         } catch (FileNotFoundException fnfe) {
-            Slog.i(LOG_TAG, "No settings state " + mStatePersistFile);
+            Slog.w(LOG_TAG, "No settings state " + mStatePersistFile);
             logSettingsDirectoryInformation(mStatePersistFile);
             addHistoricalOperationLocked(HISTORICAL_OPERATION_INITIALIZE, null);
             return;
         }
+        if (parseStateFromXmlStreamLocked(in)) {
+            return;
+        }
+
+        // Settings file exists but is corrupted. Retry with the fallback file
+        final File statePersistFallbackFile = new File(
+                mStatePersistFile.getAbsolutePath() + FALLBACK_FILE_SUFFIX);
+        Slog.i(LOG_TAG, "Failed parsing settings file: " + mStatePersistFile
+                + ", retrying with fallback file: " + statePersistFallbackFile);
+        try {
+            in = new AtomicFile(statePersistFallbackFile).openRead();
+        } catch (FileNotFoundException fnfe) {
+            final String message = "No fallback file found for: " + mStatePersistFile;
+            Slog.wtf(LOG_TAG, message);
+            throw new IllegalStateException(message);
+        }
+        if (parseStateFromXmlStreamLocked(in)) {
+            // Parsed state from fallback file. Restore original file with fallback file
+            try {
+                FileUtils.copy(statePersistFallbackFile, mStatePersistFile);
+            } catch (IOException ignored) {
+                // Failed to copy, but it's okay because we already parsed states from fallback file
+            }
+        } else {
+            final String message = "Failed parsing settings file: " + mStatePersistFile;
+            Slog.wtf(LOG_TAG, message);
+            throw new IllegalStateException(message);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean parseStateFromXmlStreamLocked(FileInputStream in) {
         try {
             XmlPullParser parser = Xml.newPullParser();
             parser.setInput(in, StandardCharsets.UTF_8.name());
             parseStateLocked(parser);
+            return true;
         } catch (XmlPullParserException | IOException e) {
-            String message = "Failed parsing settings file: " + mStatePersistFile;
-            Slog.wtf(LOG_TAG, message);
-            throw new IllegalStateException(message, e);
+            return false;
         } finally {
             IoUtils.closeQuietly(in);
         }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WriteFallbackSettingsFilesJobService.java b/packages/SettingsProvider/src/com/android/providers/settings/WriteFallbackSettingsFilesJobService.java
new file mode 100644
index 0000000..6e5b8890
--- /dev/null
+++ b/packages/SettingsProvider/src/com/android/providers/settings/WriteFallbackSettingsFilesJobService.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 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.providers.settings;
+
+import static com.android.providers.settings.SettingsProvider.TABLE_CONFIG;
+import static com.android.providers.settings.SettingsProvider.TABLE_GLOBAL;
+import static com.android.providers.settings.SettingsProvider.TABLE_SECURE;
+import static com.android.providers.settings.SettingsProvider.TABLE_SSAID;
+import static com.android.providers.settings.SettingsProvider.TABLE_SYSTEM;
+import static com.android.providers.settings.SettingsProvider.WRITE_FALLBACK_SETTINGS_FILES_JOB_ID;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JobService to make a copy of a list of files, given their paths.
+ */
+public class WriteFallbackSettingsFilesJobService extends JobService {
+    @Override
+    public boolean onStartJob(final JobParameters params) {
+        switch (params.getJobId()) {
+            case WRITE_FALLBACK_SETTINGS_FILES_JOB_ID:
+                final List<String> settingsFiles = new ArrayList<>();
+                settingsFiles.add(params.getExtras().getString(TABLE_GLOBAL, ""));
+                settingsFiles.add(params.getExtras().getString(TABLE_SYSTEM, ""));
+                settingsFiles.add(params.getExtras().getString(TABLE_SECURE, ""));
+                settingsFiles.add(params.getExtras().getString(TABLE_SSAID, ""));
+                settingsFiles.add(params.getExtras().getString(TABLE_CONFIG, ""));
+                SettingsProvider.writeFallBackSettingsFiles(settingsFiles);
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+
+}