Merge "DB Wipe detection"
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 9d3c5c6..4756bf4 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1425,60 +1425,10 @@
             PrintWriter pw = new FastPrintWriter(
                     new FileOutputStream(pfd.getFileDescriptor()));
             PrintWriterPrinter printer = new PrintWriterPrinter(pw);
-            SQLiteDebug.dump(printer, args);
-
-            if (isSystem) {
-                dumpDatabaseFileSizes(pw, Environment.getDataSystemDirectory(), true);
-                dumpDatabaseFileSizes(pw, Environment.getDataSystemDeDirectory(), true);
-                dumpDatabaseFileSizes(pw, Environment.getDataSystemCeDirectory(), true);
-            } else {
-                Context context = getApplication();
-                if (context != null) {
-                    dumpDatabaseFileSizes(pw,
-                            getDatabasesDir(context.createDeviceProtectedStorageContext()),
-                            false);
-                    dumpDatabaseFileSizes(pw,
-                            getDatabasesDir(context.createCredentialProtectedStorageContext()),
-                            false);
-                }
-            }
+            SQLiteDebug.dump(printer, args, isSystem);
             pw.flush();
         }
 
-        private void dumpDatabaseFileSizes(PrintWriter pw, File dir, boolean isSystem) {
-            final File[] files = dir.listFiles();
-            if (files == null || files.length == 0) {
-                return;
-            }
-            Arrays.sort(files, (a, b) -> a.getName().compareTo(b.getName()));
-
-            boolean needHeader = true;
-            for (File f : files) {
-                if (isSystem) {
-                    // If it's the system server, the directory contains other files too, so
-                    // filter by file extensions.
-                    // (If it's an app, just print all files because they may not use *.db
-                    // extension.)
-                    final String name = f.getName();
-                    if (!(name.endsWith(".db") || name.endsWith(".db-wal")
-                            || name.endsWith(".db-journal"))) {
-                        continue;
-                    }
-                }
-                if (needHeader) {
-                    pw.println();
-                    pw.println("Database files in " + dir.getAbsolutePath() + ":");
-                    needHeader = false;
-                }
-
-                pw.print("  ");
-                pw.print(f.getName());
-                pw.print("  ");
-                pw.print(f.length());
-                pw.println(" bytes");
-            }
-        }
-
         @Override
         public void dumpDbInfo(final ParcelFileDescriptor pfd, final String[] args) {
             if (mSystemThread) {
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 599c2d2..a2a6b9b 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -36,11 +36,9 @@
 import android.database.Cursor;
 import android.database.IContentObserver;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.ImageDecoder;
 import android.graphics.ImageDecoder.ImageInfo;
 import android.graphics.ImageDecoder.Source;
-import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
@@ -55,7 +53,6 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
-import android.provider.DocumentsContract;
 import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
@@ -3255,4 +3252,13 @@
             }
         });
     }
+
+    /** {@hide} */
+    public static void onDbCorruption(String tag, String message, Throwable stacktrace) {
+        try {
+            getContentService().onDbCorruption(tag, message, Log.getStackTraceString(stacktrace));
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl
index a55dd31..1d02375 100644
--- a/core/java/android/content/IContentService.aidl
+++ b/core/java/android/content/IContentService.aidl
@@ -185,4 +185,6 @@
     Bundle getCache(in String packageName, in Uri key, int userId);
 
     void resetTodayStats();
+
+    void onDbCorruption(String tag, String message, String stacktrace);
 }
diff --git a/core/java/android/database/DefaultDatabaseErrorHandler.java b/core/java/android/database/DefaultDatabaseErrorHandler.java
index 7fa2b40..cf019e1 100755
--- a/core/java/android/database/DefaultDatabaseErrorHandler.java
+++ b/core/java/android/database/DefaultDatabaseErrorHandler.java
@@ -15,14 +15,14 @@
  */
 package android.database;
 
-import java.io.File;
-import java.util.List;
-
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteException;
 import android.util.Log;
 import android.util.Pair;
 
+import java.io.File;
+import java.util.List;
+
 /**
  * Default class used to define the action to take when database corruption is reported
  * by sqlite.
@@ -52,6 +52,7 @@
      */
     public void onCorruption(SQLiteDatabase dbObj) {
         Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath());
+        SQLiteDatabase.wipeDetected(dbObj.getPath(), "corruption");
 
         // is the corruption detected even before database could be 'opened'?
         if (!dbObj.isOpen()) {
@@ -99,7 +100,7 @@
         }
         Log.e(TAG, "deleting the database file: " + fileName);
         try {
-            SQLiteDatabase.deleteDatabase(new File(fileName));
+            SQLiteDatabase.deleteDatabase(new File(fileName), /*removeCheckFile=*/ false);
         } catch (Exception e) {
             /* print warning and ignore exception */
             Log.w(TAG, "delete failed: " + e.getMessage());
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index 5c4f16a..20505ca 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -34,6 +34,7 @@
 import dalvik.system.CloseGuard;
 
 import java.io.File;
+import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -414,6 +415,10 @@
         final String newLocale = mConfiguration.locale.toString();
         nativeRegisterLocalizedCollators(mConnectionPtr, newLocale);
 
+        if (!mConfiguration.isInMemoryDb()) {
+            checkDatabaseWiped();
+        }
+
         // If the database is read-only, we cannot modify the android metadata table
         // or existing indexes.
         if (mIsReadOnlyConnection) {
@@ -449,6 +454,36 @@
         }
     }
 
+    private void checkDatabaseWiped() {
+        if (!SQLiteGlobal.checkDbWipe()) {
+            return;
+        }
+        try {
+            final File checkFile = new File(mConfiguration.path
+                    + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX);
+
+            final boolean hasMetadataTable = executeForLong(
+                    "SELECT count(*) FROM sqlite_master"
+                            + " WHERE type='table' AND name='android_metadata'", null, null) > 0;
+            final boolean hasCheckFile = checkFile.exists();
+
+            if (!mIsReadOnlyConnection && !hasCheckFile) {
+                // Create the check file, unless it's a readonly connection,
+                // in which case we can't create the metadata table anyway.
+                checkFile.createNewFile();
+            }
+
+            if (!hasMetadataTable && hasCheckFile) {
+                // Bad. The DB is gone unexpectedly.
+                SQLiteDatabase.wipeDetected(mConfiguration.path, "unknown");
+            }
+
+        } catch (RuntimeException | IOException ex) {
+            SQLiteDatabase.wtfAsSystemServer(TAG,
+                    "Unexpected exception while checking for wipe", ex);
+        }
+    }
+
     // Called by SQLiteConnectionPool only.
     void reconfigure(SQLiteDatabaseConfiguration configuration) {
         mOnlyAllowReadOnlyOperations = false;
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
index 3ee348b..dbc1766 100644
--- a/core/java/android/database/sqlite/SQLiteConnectionPool.java
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -24,6 +24,7 @@
 import android.os.OperationCanceledException;
 import android.os.SystemClock;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.PrefixPrinter;
 import android.util.Printer;
@@ -34,6 +35,7 @@
 import dalvik.system.CloseGuard;
 
 import java.io.Closeable;
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Map;
 import java.util.WeakHashMap;
@@ -1105,9 +1107,12 @@
      * @param printer The printer to receive the dump, not null.
      * @param verbose True to dump more verbose information.
      */
-    public void dump(Printer printer, boolean verbose) {
+    public void dump(Printer printer, boolean verbose, ArraySet<String> directories) {
         Printer indentedPrinter = PrefixPrinter.create(printer, "    ");
         synchronized (mLock) {
+            if (directories != null) {
+                directories.add(new File(mConfiguration.path).getParent());
+            }
             printer.println("Connection pool for " + mConfiguration.path + ":");
             printer.println("  Open: " + mIsOpen);
             printer.println("  Max connections: " + mMaxConnectionPoolSize);
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index eb5c720..f9c2c3e 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -22,6 +22,8 @@
 import android.annotation.Nullable;
 import android.annotation.UnsupportedAppUsage;
 import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.database.DatabaseErrorHandler;
@@ -34,6 +36,7 @@
 import android.os.OperationCanceledException;
 import android.os.SystemProperties;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Pair;
@@ -45,9 +48,14 @@
 
 import java.io.File;
 import java.io.FileFilter;
+import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -808,6 +816,12 @@
      * @return True if the database was successfully deleted.
      */
     public static boolean deleteDatabase(@NonNull File file) {
+        return deleteDatabase(file, /*removeCheckFile=*/ true);
+    }
+
+
+    /** @hide */
+    public static boolean deleteDatabase(@NonNull File file, boolean removeCheckFile) {
         if (file == null) {
             throw new IllegalArgumentException("file must not be null");
         }
@@ -818,6 +832,9 @@
         deleted |= new File(file.getPath() + "-shm").delete();
         deleted |= new File(file.getPath() + "-wal").delete();
 
+        // This file is not a standard SQLite file, so don't update the deleted flag.
+        new File(file.getPath() + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX).delete();
+
         File dir = file.getParentFile();
         if (dir != null) {
             final String prefix = file.getName() + "-mj";
@@ -2170,21 +2187,61 @@
      * Dump detailed information about all open databases in the current process.
      * Used by bug report.
      */
-    static void dumpAll(Printer printer, boolean verbose) {
+    static void dumpAll(Printer printer, boolean verbose, boolean isSystem) {
+        // Use this ArraySet to collect file paths.
+        final ArraySet<String> directories = new ArraySet<>();
+
         for (SQLiteDatabase db : getActiveDatabases()) {
-            db.dump(printer, verbose);
+            db.dump(printer, verbose, isSystem, directories);
+        }
+
+        // Dump DB files in the directories.
+        if (directories.size() > 0) {
+            final String[] dirs = directories.toArray(new String[directories.size()]);
+            Arrays.sort(dirs);
+            for (String dir : dirs) {
+                dumpDatabaseDirectory(printer, new File(dir), isSystem);
+            }
         }
     }
 
-    private void dump(Printer printer, boolean verbose) {
+    private void dump(Printer printer, boolean verbose, boolean isSystem, ArraySet directories) {
         synchronized (mLock) {
             if (mConnectionPoolLocked != null) {
                 printer.println("");
-                mConnectionPoolLocked.dump(printer, verbose);
+                mConnectionPoolLocked.dump(printer, verbose, directories);
             }
         }
     }
 
+    private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) {
+        pw.println("");
+        pw.println("Database files in " + dir.getAbsolutePath() + ":");
+        final File[] files = dir.listFiles();
+        if (files == null || files.length == 0) {
+            pw.println("  [none]");
+            return;
+        }
+        Arrays.sort(files, (a, b) -> a.getName().compareTo(b.getName()));
+
+        for (File f : files) {
+            if (isSystem) {
+                // If called within the system server, the directory contains other files too, so
+                // filter by file extensions.
+                // (If it's an app, just print all files because they may not use *.db
+                // extension.)
+                final String name = f.getName();
+                if (!(name.endsWith(".db") || name.endsWith(".db-wal")
+                        || name.endsWith(".db-journal")
+                        || name.endsWith(SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX))) {
+                    continue;
+                }
+            }
+            pw.println(String.format("  %-40s %7db %s", f.getName(), f.length(),
+                    SQLiteDatabase.getFileTimestamps(f.getAbsolutePath())));
+        }
+    }
+
     /**
      * Returns list of full pathnames of all attached databases including the main database
      * by executing 'pragma database_list' on the database.
@@ -2611,7 +2668,7 @@
                 return this;
             }
 
-            /**
+            /**w
              * Sets <a href="https://sqlite.org/pragma.html#pragma_synchronous">synchronous mode</a>
              * .
              * @return
@@ -2646,5 +2703,34 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface DatabaseOpenFlags {}
 
+    /** @hide */
+    public static void wipeDetected(String filename, String reason) {
+        wtfAsSystemServer(TAG, "DB wipe detected:"
+                + " package=" + ActivityThread.currentPackageName()
+                + " reason=" + reason
+                + " file=" + filename
+                + " " + getFileTimestamps(filename)
+                + " checkfile " + getFileTimestamps(filename + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX),
+                new Throwable("STACKTRACE"));
+    }
+
+    /** @hide */
+    public static String getFileTimestamps(String path) {
+        try {
+            BasicFileAttributes attr = Files.readAttributes(
+                    FileSystems.getDefault().getPath(path), BasicFileAttributes.class);
+            return "ctime=" + attr.creationTime()
+                    + " mtime=" + attr.lastModifiedTime()
+                    + " atime=" + attr.lastAccessTime();
+        } catch (IOException e) {
+            return "[unable to obtain timestamp]";
+        }
+    }
+
+    /** @hide */
+    static void wtfAsSystemServer(String tag, String message, Throwable stacktrace) {
+        Log.e(tag, message, stacktrace);
+        ContentResolver.onDbCorruption(tag, message, stacktrace);
+    }
 }
 
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
index 1c66204..f220205 100644
--- a/core/java/android/database/sqlite/SQLiteDebug.java
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -189,6 +189,11 @@
      * @param args Command-line arguments supplied to dumpsys dbinfo
      */
     public static void dump(Printer printer, String[] args) {
+        dump(printer, args, false);
+    }
+
+    /** @hide */
+    public static void dump(Printer printer, String[] args, boolean isSystem) {
         boolean verbose = false;
         for (String arg : args) {
             if (arg.equals("-v")) {
@@ -196,6 +201,6 @@
             }
         }
 
-        SQLiteDatabase.dumpAll(printer, verbose);
+        SQLiteDatabase.dumpAll(printer, verbose, isSystem);
     }
 }
diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java
index 67e5f65..ff286fd 100644
--- a/core/java/android/database/sqlite/SQLiteGlobal.java
+++ b/core/java/android/database/sqlite/SQLiteGlobal.java
@@ -42,6 +42,9 @@
     /** @hide */
     public static final String SYNC_MODE_FULL = "FULL";
 
+    /** @hide */
+    static final String WIPE_CHECK_FILE_SUFFIX = "-wipecheck";
+
     private static final Object sLock = new Object();
 
     private static int sDefaultPageSize;
@@ -181,4 +184,8 @@
                         com.android.internal.R.integer.db_wal_truncate_size));
     }
 
+    /** @hide */
+    public static boolean checkDbWipe() {
+        return true;
+    }
 }
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 5698fdf..5ed6263 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -1615,6 +1615,15 @@
     }
 
     @Override
+    public void onDbCorruption(String tag, String message, String stacktrace) {
+        Slog.e(tag, message);
+        Slog.e(tag, "at " + stacktrace);
+
+        // TODO: Figure out a better way to report it. b/117886381
+        Slog.wtf(tag, message);
+    }
+
+    @Override
     public void onShellCommand(FileDescriptor in, FileDescriptor out,
             FileDescriptor err, String[] args, ShellCallback callback,
             ResultReceiver resultReceiver) {