am 0c1ecf1: AI 148026: Update zh_TW translations.

Merge commit '0c1ecf18e96b48b5afb93e9f724b8c3836e7639b' into donut

* commit '0c1ecf18e96b48b5afb93e9f724b8c3836e7639b':
  AI 148026: Update zh_TW translations.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e5bccaa..cbaa89d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -6,6 +6,7 @@
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.SDCARD_WRITE" />
     
     <application android:process="android.process.media"
                  android:label="@string/app_label">
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..6215092
--- /dev/null
+++ b/res/values-es-rUS/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="uid_label">"Medios"</string>
+    <string name="app_label">"Almacenamiento de medios"</string>
+</resources>
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 1a2f5a6..f6a2cd5 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -19,7 +19,6 @@
 import android.app.SearchManager;
 import android.content.*;
 import android.database.Cursor;
-import android.database.MergeCursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
@@ -45,7 +44,6 @@
 import android.provider.MediaStore.Video;
 import android.provider.MediaStore.Images.ImageColumns;
 import android.text.TextUtils;
-import android.util.Config;
 import android.util.Log;
 
 import java.io.File;
@@ -57,7 +55,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
+import java.util.Stack;
 
 /**
  * Media content provider. See {@link android.provider.MediaStore} for details.
@@ -72,6 +70,55 @@
 
     private static final HashMap<String, String> sArtistAlbumsMap = new HashMap<String, String>();
 
+    // A HashSet of paths that are pending creation of album art thumbnails.
+    private HashSet mPendingThumbs = new HashSet();
+
+    // A Stack of outstanding thumbnail requests.
+    private Stack mThumbRequestStack = new Stack();
+
+    // For compatibility with the approximately 0 apps that used mediaprovider search in
+    // releases 1.0, 1.1 or 1.5
+    private String[] mSearchColsLegacy = new String[] {
+            android.provider.BaseColumns._ID,
+            MediaStore.Audio.Media.MIME_TYPE,
+            "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist +
+            " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album +
+            " ELSE " + R.drawable.ic_search_category_music_song + " END END" +
+            ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
+            "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2,
+            "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
+            "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY,
+            "CASE when grouporder=1 THEN data1 ELSE artist END AS data1",
+            "CASE when grouporder=1 THEN data2 ELSE " +
+                "CASE WHEN grouporder=2 THEN NULL ELSE album END END AS data2",
+            "match as ar",
+            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+            "grouporder",
+            "itemorder"
+    };
+    private String[] mSearchColsFancy = new String[] {
+            android.provider.BaseColumns._ID,
+            MediaStore.Audio.Media.MIME_TYPE,
+            MediaStore.Audio.Artists.ARTIST,
+            MediaStore.Audio.Albums.ALBUM,
+            MediaStore.Audio.Media.TITLE,
+            "data1",
+            "data2",
+    };
+    private String[] mSearchColsBasic = new String[] {
+            android.provider.BaseColumns._ID,
+            MediaStore.Audio.Media.MIME_TYPE,
+            "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist +
+            " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album +
+            " ELSE " + R.drawable.ic_search_category_music_song + " END END" +
+            ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
+            "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
+            "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY,
+            "CASE WHEN text2!='" + MediaFile.UNKNOWN_STRING + "' THEN text2 ELSE NULL END AS " +
+                SearchManager.SUGGEST_COLUMN_TEXT_2,
+            SearchManager.SUGGEST_COLUMN_INTENT_DATA
+    };
+
     private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -224,7 +271,18 @@
         mThumbHandler = new Handler(mThumbWorker.getLooper()) {
             @Override
             public void handleMessage(Message msg) {
-                makeThumb((ThumbData)msg.obj);
+                // The message itself is irrelevant. We are going to pop
+                // a thumbnail request off the stack in response.
+
+                ThumbData d;
+                synchronized (mThumbRequestStack) {
+                    d = (ThumbData)mThumbRequestStack.pop();
+                }
+
+                makeThumb(d);
+                synchronized (mPendingThumbs) {
+                    mPendingThumbs.remove(d.path);
+                }
             }
         };
 
@@ -561,6 +619,76 @@
             // To work around this, we drop and recreate the affected view and trigger.
             recreateAudioView(db);
         }
+        
+        if (fromVersion < 73) {
+            // There is no change to the database schema, but we now do case insensitive
+            // matching of folder names when determining whether something is music, a
+            // ringtone, podcast, etc, so we might need to reclassify some files.
+            db.execSQL("UPDATE audio_meta SET is_music=1 WHERE is_music=0 AND " +
+                    "_data LIKE '%/music/%';");
+            db.execSQL("UPDATE audio_meta SET is_ringtone=1 WHERE is_ringtone=0 AND " +
+                    "_data LIKE '%/ringtones/%';");
+            db.execSQL("UPDATE audio_meta SET is_notification=1 WHERE is_notification=0 AND " +
+                    "_data LIKE '%/notifications/%';");
+            db.execSQL("UPDATE audio_meta SET is_alarm=1 WHERE is_alarm=0 AND " +
+                    "_data LIKE '%/alarms/%';");
+            db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE is_podcast=0 AND " +
+                    "_data LIKE '%/podcasts/%';");
+        }
+
+        if (fromVersion < 74) {
+            // This view is used instead of the audio view by the union below, to force
+            // sqlite to use the title_key index. This greatly reduces memory usage
+            // (no separate copy pass needed for sorting, which could cause errors on
+            // large datasets) and improves speed (by about 35% on a large dataset)
+            db.execSQL("CREATE VIEW IF NOT EXISTS searchhelpertitle AS SELECT * FROM audio " +
+                    "ORDER BY title_key;");
+
+            db.execSQL("CREATE VIEW IF NOT EXISTS search AS " +
+                    "SELECT _id," +
+                    "'artist' AS mime_type," +
+                    "artist," +
+                    "NULL AS album," +
+                    "NULL AS title," +
+                    "artist AS text1," +
+                    "NULL AS text2," +
+                    "number_of_albums AS data1," +
+                    "number_of_tracks AS data2," +
+                    "artist_key AS match," +
+                    "'content://media/external/audio/artists/'||_id AS suggest_intent_data," +
+                    "1 AS grouporder " +
+                    "FROM artist_info WHERE (artist!='" + MediaFile.UNKNOWN_STRING + "') " +
+                "UNION ALL " +
+                    "SELECT _id," +
+                    "'album' AS mime_type," +
+                    "artist," +
+                    "album," +
+                    "NULL AS title," +
+                    "album AS text1," +
+                    "artist AS text2," +
+                    "NULL AS data1," +
+                    "NULL AS data2," +
+                    "artist_key||' '||album_key AS match," +
+                    "'content://media/external/audio/albums/'||_id AS suggest_intent_data," +
+                    "2 AS grouporder " +
+                    "FROM album_info WHERE (album!='" + MediaFile.UNKNOWN_STRING + "') " +
+                "UNION ALL " +
+                    "SELECT searchhelpertitle._id AS _id," +
+                    "mime_type," +
+                    "artist," +
+                    "album," +
+                    "title," +
+                    "title AS text1," +
+                    "artist AS text2," +
+                    "NULL AS data1," +
+                    "NULL AS data2," +
+                    "artist_key||' '||album_key||' '||title_key AS match," +
+                    "'content://media/external/audio/media/'||searchhelpertitle._id AS " +
+                    "suggest_intent_data," +
+                    "3 AS grouporder " +
+                    "FROM searchhelpertitle WHERE (title != '') "
+                    );
+        }
     }
 
     private static void recreateAudioView(SQLiteDatabase db) {
@@ -801,9 +929,11 @@
                 break;
 
             case AUDIO_PLAYLISTS_ID_MEMBERS:
-                for (int i = 0; i < projectionIn.length; i++) {
-                    if (projectionIn[i].equals("_id")) {
-                        projectionIn[i] = "audio_playlists_map._id AS _id";
+                if (projectionIn != null) {
+                    for (int i = 0; i < projectionIn.length; i++) {
+                        if (projectionIn[i].equals("_id")) {
+                            projectionIn[i] = "audio_playlists_map._id AS _id";
+                        }
                     }
                 }
                 qb.setTables("audio_playlists_map, audio");
@@ -862,8 +992,13 @@
                 qb.appendWhere("album_id=" + uri.getPathSegments().get(3));
                 break;
 
-            case AUDIO_SEARCH:
-                return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort);
+            case AUDIO_SEARCH_LEGACY:
+                Log.w(TAG, "Legacy media search Uri used. Please update your code.");
+                // fall through
+            case AUDIO_SEARCH_FANCY:
+            case AUDIO_SEARCH_BASIC:
+                return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort,
+                        table);
 
             default:
                 throw new IllegalStateException("Unknown URL: " + uri.toString());
@@ -879,136 +1014,45 @@
 
     private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb,
             Uri uri, String[] projectionIn, String selection,
-            String[] selectionArgs, String sort) {
+            String[] selectionArgs, String sort, int mode) {
 
-        List<String> l = uri.getPathSegments();
-        String mSearchString = l.size() == 4 ? l.get(3) : "";
+        String mSearchString = uri.toString().endsWith("/") ? "" : uri.getLastPathSegment();
         mSearchString = mSearchString.replaceAll("  ", " ").trim().toLowerCase();
-        Cursor mCursor = null;
 
         String [] searchWords = mSearchString.length() > 0 ?
                 mSearchString.split(" ") : new String[0];
-        String [] wildcardWords3 = new String[searchWords.length * 3];
+        String [] wildcardWords = new String[searchWords.length];
         Collator col = Collator.getInstance();
         col.setStrength(Collator.PRIMARY);
         int len = searchWords.length;
         for (int i = 0; i < len; i++) {
             // Because we match on individual words here, we need to remove words
             // like 'a' and 'the' that aren't part of the keys.
-            wildcardWords3[i] = wildcardWords3[i + len] = wildcardWords3[i + len + len] =
+            wildcardWords[i] =
                 (searchWords[i].equals("a") || searchWords[i].equals("an") ||
                         searchWords[i].equals("the")) ? "%" :
                 '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
         }
 
-        String UQs [] = new String[3];
-        HashSet<String> tablecolumns = new HashSet<String>();
-
-        // Direct match artists
-        {
-            String[] ccols = new String[] {
-                    MediaStore.Audio.Artists._ID,
-                    "'artist' AS " + MediaStore.Audio.Media.MIME_TYPE,
-                    "" + R.drawable.ic_search_category_music_artist + " AS " +
-                        SearchManager.SUGGEST_COLUMN_ICON_1,
-                    "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2,
-                    MediaStore.Audio.Artists.ARTIST + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
-                    MediaStore.Audio.Artists.ARTIST + " AS " + SearchManager.SUGGEST_COLUMN_QUERY,
-                    MediaStore.Audio.Artists.NUMBER_OF_ALBUMS + " AS data1",
-                    MediaStore.Audio.Artists.NUMBER_OF_TRACKS + " AS data2",
-                    MediaStore.Audio.Artists.ARTIST_KEY + " AS ar",
-                    "'content://media/external/audio/artists/'||" + MediaStore.Audio.Artists._ID +
-                    " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA,
-                    "'1' AS grouporder",
-                    "artist_key AS itemorder"
-            };
-
-
-            String where = MediaStore.Audio.Artists.ARTIST_KEY + " != ''";
-            for (int i = 0; i < searchWords.length; i++) {
-                where += " AND ar LIKE ?";
+        String where = "";
+        for (int i = 0; i < searchWords.length; i++) {
+            if (i == 0) {
+                where = "match LIKE ?";
+            } else {
+                where += " AND match LIKE ?";
             }
-
-            qb.setTables("artist_info");
-            UQs[0] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE,
-                    ccols, tablecolumns, ccols.length, "artist", where, null, null, null);
         }
 
-        // Direct match albums
-        {
-            String[] ccols = new String[] {
-                    MediaStore.Audio.Albums._ID,
-                    "'album' AS " + MediaStore.Audio.Media.MIME_TYPE,
-                    "" + R.drawable.ic_search_category_music_album + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
-                    "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2,
-                    MediaStore.Audio.Albums.ALBUM + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
-                    MediaStore.Audio.Albums.ALBUM + " AS " + SearchManager.SUGGEST_COLUMN_QUERY,
-                    MediaStore.Audio.Media.ARTIST + " AS data1",
-                    "null AS data2",
-                    MediaStore.Audio.Media.ARTIST_KEY +
-                    "||' '||" +
-                    MediaStore.Audio.Media.ALBUM_KEY +
-                    " AS ar_al",
-                    "'content://media/external/audio/albums/'||" + MediaStore.Audio.Albums._ID +
-                    " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA,
-                    "'2' AS grouporder",
-                    "album_key AS itemorder"
-            };
-
-            String where = MediaStore.Audio.Media.ALBUM_KEY + " != ''";
-            for (int i = 0; i < searchWords.length; i++) {
-                where += " AND ar_al LIKE ?";
-            }
-
-            qb = new SQLiteQueryBuilder();
-            qb.setTables("album_info");
-            UQs[1] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE,
-                    ccols, tablecolumns, ccols.length, "album", where, null, null, null);
+        qb.setTables("search");
+        String [] cols;
+        if (mode == AUDIO_SEARCH_FANCY) {
+            cols = mSearchColsFancy;
+        } else if (mode == AUDIO_SEARCH_BASIC) {
+            cols = mSearchColsBasic;
+        } else {
+            cols = mSearchColsLegacy;
         }
-
-        // Direct match tracks
-        {
-            String[] ccols = new String[] {
-                    "audio._id AS _id",
-                    MediaStore.Audio.Media.MIME_TYPE,
-                    "" + R.drawable.ic_search_category_music_song + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
-                    "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2,
-                    MediaStore.Audio.Media.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
-                    MediaStore.Audio.Media.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_QUERY,
-                    MediaStore.Audio.Media.ARTIST + " AS data1",
-                    MediaStore.Audio.Media.ALBUM + " AS data2",
-                    MediaStore.Audio.Media.ARTIST_KEY +
-                    "||' '||" +
-                    MediaStore.Audio.Media.ALBUM_KEY +
-                    "||' '||" +
-                    MediaStore.Audio.Media.TITLE_KEY +
-                    " AS ar_al_ti",
-                    "'content://media/external/audio/media/'||audio._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA,
-                    "'3' AS grouporder",
-                    "title_key AS itemorder"
-            };
-
-            String where = MediaStore.Audio.Media.TITLE + " != ''";
-
-            for (int i = 0; i < searchWords.length; i++) {
-                where += " AND ar_al_ti LIKE ?";
-            }
-            qb = new SQLiteQueryBuilder();
-            qb.setTables("audio");
-            UQs[2] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE,
-                    ccols, tablecolumns, ccols.length, "audio/", where, null, null, null);
-        }
-
-        if (mCursor != null) {
-            mCursor.deactivate();
-            mCursor = null;
-        }
-        if (UQs[0] != null && UQs[1] != null && UQs[2] != null) {
-            String union = qb.buildUnionQuery(UQs, "grouporder,itemorder", null);
-            mCursor = db.rawQuery(union, wildcardWords3);
-        }
-
-        return mCursor;
+        return qb.query(db, cols, where, wildcardWords, null, null, null);
     }
 
     @Override
@@ -1727,45 +1771,64 @@
 
     private void makeThumb(SQLiteDatabase db, String path, long album_id,
             Uri albumart_uri) {
+        synchronized (mPendingThumbs) {
+            if (mPendingThumbs.contains(path)) {
+                // There's already a request to make an album art thumbnail
+                // for this audio file in the queue.
+                return;
+            }
+
+            mPendingThumbs.add(path);
+        }
+
         ThumbData d = new ThumbData();
         d.db = db;
         d.path = path;
         d.album_id = album_id;
         d.albumart_uri = albumart_uri;
+
+        // Instead of processing thumbnail requests in the order they were
+        // received we instead process them stack-based, i.e. LIFO.
+        // The idea behind this is that the most recently requested thumbnails
+        // are most likely the ones still in the user's view, whereas those
+        // requested earlier may have already scrolled off.
+        synchronized (mThumbRequestStack) {
+            mThumbRequestStack.push(d);
+        }
+
+        // Trigger the handler.
         Message msg = mThumbHandler.obtainMessage();
-        msg.obj = d;
         msg.sendToTarget();
     }
 
-    private void makeThumb(ThumbData d) {
-        SQLiteDatabase db = d.db;
-        String path = d.path;
-        long album_id = d.album_id;
-        Uri albumart_uri = d.albumart_uri;
+    // Extract compressed image data from the audio file itself or, if that fails,
+    // look for a file "AlbumArt.jpg" in the containing directory.
+    private static byte[] getCompressedAlbumArt(Context context, String path) {
+        byte[] compressed = null;
 
         try {
             File f = new File(path);
             ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f,
                     ParcelFileDescriptor.MODE_READ_ONLY);
 
-            MediaScanner scanner = new MediaScanner(getContext());
-            byte [] art = scanner.extractAlbumArt(pfd.getFileDescriptor());
+            MediaScanner scanner = new MediaScanner(context);
+            compressed = scanner.extractAlbumArt(pfd.getFileDescriptor());
             pfd.close();
 
             // if no embedded art exists, look for AlbumArt.jpg in same directory as the media file
-            if (art == null && path != null) {
+            if (compressed == null && path != null) {
                 int lastSlash = path.lastIndexOf('/');
                 if (lastSlash > 0) {
                     String artPath = path.substring(0, lastSlash + 1) + "AlbumArt.jpg";
                     File file = new File(artPath);
                     if (file.exists()) {
-                        art = new byte[(int)file.length()];
+                        compressed = new byte[(int)file.length()];
                         FileInputStream stream = null;
                         try {
                             stream = new FileInputStream(file);
-                            stream.read(art);
+                            stream.read(compressed);
                         } catch (IOException ex) {
-                            art = null;
+                            compressed = null;
                         } finally {
                             if (stream != null) {
                                 stream.close();
@@ -1774,84 +1837,126 @@
                     }
                 }
             }
-
-            Bitmap bm = null;
-            if (art != null) {
-                try {
-                    // get the size of the bitmap
-                    BitmapFactory.Options opts = new BitmapFactory.Options();
-                    opts.inJustDecodeBounds = true;
-                    opts.inSampleSize = 1;
-                    BitmapFactory.decodeByteArray(art, 0, art.length, opts);
-
-                    // request a reasonably sized output image
-                    // TODO: don't hardcode the size
-                    while (opts.outHeight > 320 || opts.outWidth > 320) {
-                        opts.outHeight /= 2;
-                        opts.outWidth /= 2;
-                        opts.inSampleSize *= 2;
-                    }
-
-                    // get the image for real now
-                    opts.inJustDecodeBounds = false;
-                    opts.inPreferredConfig = Bitmap.Config.RGB_565;
-                    bm = BitmapFactory.decodeByteArray(art, 0, art.length, opts);
-                } catch (Exception e) {
-                }
-            }
-            if (bm != null && bm.getConfig() == null) {
-                bm = bm.copy(Bitmap.Config.RGB_565, false);
-            }
-            if (bm != null) {
-                // save bitmap
-                Uri out = null;
-                // TODO: this could be done more efficiently with a call to db.replace(), which
-                // replaces or inserts as needed, making it unnecessary to query() first.
-                if (albumart_uri != null) {
-                    Cursor c = query(albumart_uri, new String [] { "_data" },
-                            null, null, null);
-                    c.moveToFirst();
-                    if (!c.isAfterLast()) {
-                        String albumart_path = c.getString(0);
-                        if (ensureFileExists(albumart_path)) {
-                            out = albumart_uri;
-                        }
-                    }
-                    c.close();
-                } else {
-                    ContentValues initialValues = new ContentValues();
-                    initialValues.put("album_id", album_id);
-                    try {
-                        ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER);
-                        long rowId = db.insert("album_art", "_data", values);
-                        if (rowId > 0) {
-                            out = ContentUris.withAppendedId(ALBUMART_URI, rowId);
-                        }
-                    } catch (IllegalStateException ex) {
-                        Log.e(TAG, "error creating album thumb file");
-                    }
-                }
-                if (out != null) {
-                    boolean success = false;
-                    try {
-                        OutputStream outstream = getContext().getContentResolver().openOutputStream(out);
-                        success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
-                        outstream.close();
-                    } catch (FileNotFoundException ex) {
-                        Log.e(TAG, "error creating file", ex);
-                    } catch (IOException ex) {
-                        Log.e(TAG, "error creating file", ex);
-                    }
-                    if (!success) {
-                        // the thumbnail was not written successfully, delete the entry that refers to it
-                        getContext().getContentResolver().delete(out, null, null);
-                    }
-                }
-                getContext().getContentResolver().notifyChange(MEDIA_URI, null);
-            }
-        } catch (IOException ex) {
+        } catch (IOException e) {
         }
 
+        return compressed;
+    }
+
+    // Return a URI to write the album art to and update the database as necessary.
+    Uri getAlbumArtOutputUri(SQLiteDatabase db, long album_id, Uri albumart_uri) {
+        Uri out = null;
+        // TODO: this could be done more efficiently with a call to db.replace(), which
+        // replaces or inserts as needed, making it unnecessary to query() first.
+        if (albumart_uri != null) {
+            Cursor c = query(albumart_uri, new String [] { "_data" },
+                    null, null, null);
+            c.moveToFirst();
+            if (!c.isAfterLast()) {
+                String albumart_path = c.getString(0);
+                if (ensureFileExists(albumart_path)) {
+                    out = albumart_uri;
+                }
+            }
+            c.close();
+        } else {
+            ContentValues initialValues = new ContentValues();
+            initialValues.put("album_id", album_id);
+            try {
+                ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER);
+                long rowId = db.insert("album_art", "_data", values);
+                if (rowId > 0) {
+                    out = ContentUris.withAppendedId(ALBUMART_URI, rowId);
+                }
+            } catch (IllegalStateException ex) {
+                Log.e(TAG, "error creating album thumb file");
+            }
+        }
+
+        return out;
+    }
+
+    // Write out the album art to the output URI, recompresses the given Bitmap
+    // if necessary, otherwise writes the compressed data.
+    private void writeAlbumArt(
+            boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm) {
+        boolean success = false;
+        try {
+            OutputStream outstream = getContext().getContentResolver().openOutputStream(out);
+
+            if (!need_to_recompress) {
+                // No need to recompress here, just write out the original
+                // compressed data here.
+                outstream.write(compressed);
+                success = true;
+            } else {
+                success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
+            }
+
+            outstream.close();
+        } catch (FileNotFoundException ex) {
+            Log.e(TAG, "error creating file", ex);
+        } catch (IOException ex) {
+            Log.e(TAG, "error creating file", ex);
+        }
+        if (!success) {
+            // the thumbnail was not written successfully, delete the entry that refers to it
+            getContext().getContentResolver().delete(out, null, null);
+        }
+    }
+
+    private void makeThumb(ThumbData d) {
+        byte[] compressed = getCompressedAlbumArt(getContext(), d.path);
+
+        if (compressed == null) {
+            return;
+        }
+
+        Bitmap bm = null;
+        boolean need_to_recompress = true;
+
+        try {
+            // get the size of the bitmap
+            BitmapFactory.Options opts = new BitmapFactory.Options();
+            opts.inJustDecodeBounds = true;
+            opts.inSampleSize = 1;
+            BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts);
+
+            // request a reasonably sized output image
+            // TODO: don't hardcode the size
+            while (opts.outHeight > 320 || opts.outWidth > 320) {
+                opts.outHeight /= 2;
+                opts.outWidth /= 2;
+                opts.inSampleSize *= 2;
+            }
+
+            if (opts.inSampleSize == 1) {
+                // The original album art was of proper size, we won't have to
+                // recompress the bitmap later.
+                need_to_recompress = false;
+            } else {
+                // get the image for real now
+                opts.inJustDecodeBounds = false;
+                opts.inPreferredConfig = Bitmap.Config.RGB_565;
+                bm = BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts);
+
+                if (bm != null && bm.getConfig() == null) {
+                    bm = bm.copy(Bitmap.Config.RGB_565, false);
+                }
+            }
+        } catch (Exception e) {
+        }
+
+        if (need_to_recompress && bm == null) {
+            return;
+        }
+
+        Uri out = getAlbumArtOutputUri(d.db, d.album_id, d.albumart_uri);
+
+        if (out != null) {
+            writeAlbumArt(need_to_recompress, out, compressed, bm);
+            getContext().getContentResolver().notifyChange(MEDIA_URI, null);
+        }
     }
 
     /**
@@ -2112,7 +2217,7 @@
 
     private static String TAG = "MediaProvider";
     private static final boolean LOCAL_LOGV = true;
-    private static final int DATABASE_VERSION = 72;
+    private static final int DATABASE_VERSION = 74;
     private static final String INTERNAL_DATABASE_NAME = "internal.db";
 
     // maximum number of cached external databases to keep
@@ -2170,7 +2275,9 @@
     private static final int VOLUMES = 300;
     private static final int VOLUMES_ID = 301;
 
-    private static final int AUDIO_SEARCH = 400;
+    private static final int AUDIO_SEARCH_LEGACY = 400;
+    private static final int AUDIO_SEARCH_BASIC = 401;
+    private static final int AUDIO_SEARCH_FANCY = 402;
 
     private static final int MEDIA_SCANNER = 500;
 
@@ -2232,9 +2339,22 @@
         URI_MATCHER.addURI("media", "*", VOLUMES_ID);
         URI_MATCHER.addURI("media", null, VOLUMES);
 
+        /**
+         * @deprecated use the 'basic' or 'fancy' search Uris instead
+         */
         URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY,
-                AUDIO_SEARCH);
+                AUDIO_SEARCH_LEGACY);
         URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
-                AUDIO_SEARCH);
+                AUDIO_SEARCH_LEGACY);
+
+        // used for search suggestions
+        URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY,
+                AUDIO_SEARCH_BASIC);
+        URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY +
+                "/*", AUDIO_SEARCH_BASIC);
+
+        // used by the music app's search activity
+        URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY);
+        URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY);
     }
 }