Merge "Performance improvements for db schema update."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4a7367c..ba475c6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -15,6 +15,16 @@
         <provider android:name="MediaProvider" android:authorities="media"
                 android:multiprocess="false" />
 
+        <!-- Handles database upgrades after OTAs, then disables itself -->
+        <receiver android:name="MediaUpgradeReceiver">
+            <!-- This broadcast is sent after the core system has finished
+                 booting, before the home app is launched or BOOT_COMPLETED
+                 is sent. -->
+            <intent-filter>
+                <action android:name="android.intent.action.PRE_BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+
         <receiver android:name="MediaScannerReceiver">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 634d21b..fc70b73 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -261,21 +261,27 @@
      * external card, or with internal storage).  Can open the actual database
      * on demand, create and upgrade the schema, etc.
      */
-    private final class DatabaseHelper extends SQLiteOpenHelper {
+    static final class DatabaseHelper extends SQLiteOpenHelper {
         final Context mContext;
         final String mName;
         final boolean mInternal;  // True if this is the internal database
+        final boolean mEarlyUpgrade;
+        final SQLiteDatabase.CustomFunction mObjectRemovedCallback;
         boolean mUpgradeAttempted; // Used for upgrade error handling
 
         // In memory caches of artist and album data.
         HashMap<String, Long> mArtistCache = new HashMap<String, Long>();
         HashMap<String, Long> mAlbumCache = new HashMap<String, Long>();
 
-        public DatabaseHelper(Context context, String name, boolean internal) {
+        public DatabaseHelper(Context context, String name, boolean internal,
+                boolean earlyUpgrade,
+                SQLiteDatabase.CustomFunction objectRemovedCallback) {
             super(context, name, null, DATABASE_VERSION);
             mContext = context;
             mName = name;
             mInternal = internal;
+            mEarlyUpgrade = earlyUpgrade;
+            mObjectRemovedCallback = objectRemovedCallback;
         }
 
         /**
@@ -334,7 +340,11 @@
 
             if (mInternal) return;  // The internal database is kept separately.
 
-            db.addCustomFunction("_OBJECT_REMOVED", 1, mObjectRemovedCallback);
+            if (mEarlyUpgrade) return; // Doing early upgrade.
+
+            if (mObjectRemovedCallback != null) {
+                db.addCustomFunction("_OBJECT_REMOVED", 1, mObjectRemovedCallback);
+            }
 
             // the code below is only needed on devices with removable storage
             if (!Environment.isExternalStorageRemovable()) return;
@@ -4224,6 +4234,26 @@
         return null;
     }
 
+    static boolean isMediaDatabaseName(String name) {
+        if (INTERNAL_DATABASE_NAME.equals(name)) {
+            return true;
+        }
+        if (EXTERNAL_DATABASE_NAME.equals(name)) {
+            return true;
+        }
+        if (name.startsWith("external-")) {
+            return true;
+        }
+        return false;
+    }
+
+    static boolean isInternalMediaDatabaseName(String name) {
+        if (INTERNAL_DATABASE_NAME.equals(name)) {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Attach the database for a volume (internal or external).
      * Does nothing if the volume is already attached, otherwise
@@ -4246,7 +4276,8 @@
             Context context = getContext();
             DatabaseHelper db;
             if (INTERNAL_VOLUME.equals(volume)) {
-                db = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true);
+                db = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true,
+                        false, mObjectRemovedCallback);
             } else if (EXTERNAL_VOLUME.equals(volume)) {
                 if (Environment.isExternalStorageRemovable()) {
                     String path = mExternalStoragePaths[0];
@@ -4255,7 +4286,8 @@
 
                     // generate database name based on volume ID
                     String dbName = "external-" + Integer.toHexString(volumeID) + ".db";
-                    db = new DatabaseHelper(context, dbName, false);
+                    db = new DatabaseHelper(context, dbName, false,
+                            false, mObjectRemovedCallback);
                     mVolumeId = volumeID;
                 } else {
                     // external database name should be EXTERNAL_DATABASE_NAME
@@ -4295,7 +4327,8 @@
                         }
                         // else DatabaseHelper will create one named EXTERNAL_DATABASE_NAME
                     }
-                    db = new DatabaseHelper(context, dbFile.getName(), false);
+                    db = new DatabaseHelper(context, dbFile.getName(), false,
+                            false, mObjectRemovedCallback);
                 }
             } else {
                 throw new IllegalArgumentException("There is no volume named " + volume);
diff --git a/src/com/android/providers/media/MediaUpgradeReceiver.java b/src/com/android/providers/media/MediaUpgradeReceiver.java
new file mode 100644
index 0000000..0e5a45c
--- /dev/null
+++ b/src/com/android/providers/media/MediaUpgradeReceiver.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2011 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.media;
+
+import java.io.File;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+import android.util.Slog;
+
+/**
+ * This will be launched during system boot, after the core system has
+ * been brought up but before any non-persistent processes have been
+ * started.  It is launched in a special state, with no content provider
+ * or custom application class associated with the process running.
+ *
+ * It's job is to prime the contacts database. Either create it
+ * if it doesn't exist, or open it and force any necessary upgrades.
+ * All of this heavy lifting happens before the boot animation ends.
+ */
+public class MediaUpgradeReceiver extends BroadcastReceiver {
+    static final String TAG = "MediaUpgradeReceiver";
+    static final String PREF_DB_VERSION = "db_version";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        // We are now running with the system up, but no apps started,
+        // so can do whatever cleanup after an upgrade that we want.
+
+        try {
+            File dbDir = context.getDatabasePath("foo").getParentFile();
+            String[] files = dbDir.list();
+            for (int i=0; i<files.length; i++) {
+                String file = files[i];
+                if (MediaProvider.isMediaDatabaseName(file)) {
+                    long startTime = System.currentTimeMillis();
+                    Slog.i(TAG, "---> Start upgrade of media database " + file);
+                    SQLiteDatabase db = null;
+                    try {
+                        MediaProvider.DatabaseHelper helper = new MediaProvider.DatabaseHelper(
+                                context, file, MediaProvider.isInternalMediaDatabaseName(file),
+                                false, null);
+                        db = helper.getWritableDatabase();
+                    } catch (Throwable t) {
+                        Log.wtf(TAG, "Error during upgrade of media db " + file, t);
+                    } finally {
+                        if (db != null) {
+                            db.close();
+                        }
+                    }
+                    Slog.i(TAG, "<--- Finished upgrade of media database " + file
+                            + " in " + (System.currentTimeMillis()-startTime) + "ms");
+                }
+            }
+        } catch (Throwable t) {
+            // Something has gone terribly wrong.
+            Log.wtf(TAG, "Error during upgrade attempt.", t);
+        }
+    }
+}