Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.providers.media;
     18 
     19 import android.app.SearchManager;
     20 import android.content.*;
     21 import android.database.AbstractCursor;
     22 import android.database.Cursor;
     23 import android.database.DatabaseUtils;
     24 import android.database.MatrixCursor;
     25 import android.database.SQLException;
     26 import android.database.sqlite.SQLiteDatabase;
     27 import android.database.sqlite.SQLiteOpenHelper;
     28 import android.database.sqlite.SQLiteQueryBuilder;
     29 import android.graphics.Bitmap;
     30 import android.graphics.BitmapFactory;
     31 import android.media.MediaScanner;
     32 import android.media.MiniThumbFile;
     33 import android.net.Uri;
     34 import android.os.Binder;
     35 import android.os.Environment;
     36 import android.os.FileUtils;
     37 import android.os.Handler;
     38 import android.os.HandlerThread;
     39 import android.os.Looper;
     40 import android.os.MemoryFile;
     41 import android.os.Message;
     42 import android.os.ParcelFileDescriptor;
     43 import android.os.Process;
     44 import android.provider.BaseColumns;
     45 import android.provider.MediaStore;
     46 import android.provider.MediaStore.Audio;
     47 import android.provider.MediaStore.Images;
     48 import android.provider.MediaStore.MediaColumns;
     49 import android.provider.MediaStore.Video;
     50 import android.provider.MediaStore.Images.ImageColumns;
     51 import android.text.TextUtils;
     52 import android.util.Log;
     53 
     54 import java.io.File;
     55 import java.io.FileInputStream;
     56 import java.io.FileNotFoundException;
     57 import java.io.IOException;
     58 import java.io.OutputStream;
     59 import java.text.Collator;
     60 import java.util.ArrayList;
     61 import java.util.HashMap;
     62 import java.util.HashSet;
     63 import java.util.Iterator;
     64 import java.util.List;
     65 import java.util.PriorityQueue;
     66 import java.util.Stack;
     67 
     68 /**
     69  * Media content provider. See {@link android.provider.MediaStore} for details.
     70  * Separate databases are kept for each external storage card we see (using the
     71  * card's ID as an index).  The content visible at content://media/external/...
     72  * changes with the card.
     73  */
     74 public class MediaProvider extends ContentProvider {
     75     private static final Uri MEDIA_URI = Uri.parse("content://media");
     76     private static final Uri ALBUMART_URI = Uri.parse("content://media/external/audio/albumart");
     77     private static final int ALBUM_THUMB = 1;
     78     private static final int IMAGE_THUMB = 2;
     79 
     80     private static final HashMap<String, String> sArtistAlbumsMap = new HashMap<String, String>();
     81     private static final HashMap<String, String> sFolderArtMap = new HashMap<String, String>();
     82 
     83     // A HashSet of paths that are pending creation of album art thumbnails.
     84     private HashSet mPendingThumbs = new HashSet();
     85 
     86     // A Stack of outstanding thumbnail requests.
     87     private Stack mThumbRequestStack = new Stack();
     88 
     89     // The lock of mMediaThumbQueue protects both mMediaThumbQueue and mCurrentThumbRequest.
     90     private MediaThumbRequest mCurrentThumbRequest = null;
     91     private PriorityQueue<MediaThumbRequest> mMediaThumbQueue =
     92             new PriorityQueue<MediaThumbRequest>(MediaThumbRequest.PRIORITY_NORMAL,
     93             MediaThumbRequest.getComparator());
     94 
     95     // For compatibility with the approximately 0 apps that used mediaprovider search in
     96     // releases 1.0, 1.1 or 1.5
     97     private String[] mSearchColsLegacy = new String[] {
     98             android.provider.BaseColumns._ID,
     99             MediaStore.Audio.Media.MIME_TYPE,
    100             "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist +
    101             " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album +
    102             " ELSE " + R.drawable.ic_search_category_music_song + " END END" +
    103             ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
    104             "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2,
    105             "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
    106             "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY,
    107             "CASE when grouporder=1 THEN data1 ELSE artist END AS data1",
    108             "CASE when grouporder=1 THEN data2 ELSE " +
    109                 "CASE WHEN grouporder=2 THEN NULL ELSE album END END AS data2",
    110             "match as ar",
    111             SearchManager.SUGGEST_COLUMN_INTENT_DATA,
    112             "grouporder",
    113             "NULL AS itemorder" // We should be sorting by the artist/album/title keys, but that
    114                                 // column is not available here, and the list is already sorted.
    115     };
    116     private String[] mSearchColsFancy = new String[] {
    117             android.provider.BaseColumns._ID,
    118             MediaStore.Audio.Media.MIME_TYPE,
    119             MediaStore.Audio.Artists.ARTIST,
    120             MediaStore.Audio.Albums.ALBUM,
    121             MediaStore.Audio.Media.TITLE,
    122             "data1",
    123             "data2",
    124     };
    125     // If this array gets changed, please update the constant below to point to the correct item.
    126     private String[] mSearchColsBasic = new String[] {
    127             android.provider.BaseColumns._ID,
    128             MediaStore.Audio.Media.MIME_TYPE,
    129             "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist +
    130             " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album +
    131             " ELSE " + R.drawable.ic_search_category_music_song + " END END" +
    132             ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
    133             "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
    134             "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY,
    135             "(CASE WHEN grouporder=1 THEN '%1'" +  // %1 gets replaced with localized string.
    136             " ELSE CASE WHEN grouporder=3 THEN artist || ' - ' || album" +
    137             " ELSE CASE WHEN text2!='" + MediaStore.UNKNOWN_STRING + "' THEN text2" +
    138             " ELSE NULL END END END) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2,
    139             SearchManager.SUGGEST_COLUMN_INTENT_DATA
    140     };
    141     // Position of the TEXT_2 item in the above array.
    142     private final int SEARCH_COLUMN_BASIC_TEXT2 = 5;
    143 
    144     private Uri mAlbumArtBaseUri = Uri.parse("content://media/external/audio/albumart");
    145 
    146     private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() {
    147         @Override
    148         public void onReceive(Context context, Intent intent) {
    149             if (intent.getAction().equals(Intent.ACTION_MEDIA_EJECT)) {
    150                 // Remove the external volume and then notify all cursors backed by
    151                 // data on that volume
    152                 detachVolume(Uri.parse("content://media/external"));
    153                 sFolderArtMap.clear();
    154                 MiniThumbFile.reset();
    155             }
    156         }
    157     };
    158 
    159     /**
    160      * Wrapper class for a specific database (associated with one particular
    161      * external card, or with internal storage).  Can open the actual database
    162      * on demand, create and upgrade the schema, etc.
    163      */
    164     private static final class DatabaseHelper extends SQLiteOpenHelper {
    165         final Context mContext;
    166         final boolean mInternal;  // True if this is the internal database
    167 
    168         // In memory caches of artist and album data.
    169         HashMap<String, Long> mArtistCache = new HashMap<String, Long>();
    170         HashMap<String, Long> mAlbumCache = new HashMap<String, Long>();
    171 
    172         public DatabaseHelper(Context context, String name, boolean internal) {
    173             super(context, name, null, DATABASE_VERSION);
    174             mContext = context;
    175             mInternal = internal;
    176         }
    177 
    178         /**
    179          * Creates database the first time we try to open it.
    180          */
    181         @Override
    182         public void onCreate(final SQLiteDatabase db) {
    183             updateDatabase(db, mInternal, 0, DATABASE_VERSION);
    184         }
    185 
    186         /**
    187          * Updates the database format when a new content provider is used
    188          * with an older database format.
    189          */
    190         @Override
    191         public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
    192             updateDatabase(db, mInternal, oldV, newV);
    193         }
    194 
    195         /**
    196          * Touch this particular database and garbage collect old databases.
    197          * An LRU cache system is used to clean up databases for old external
    198          * storage volumes.
    199          */
    200         @Override
    201         public void onOpen(SQLiteDatabase db) {
    202             if (mInternal) return;  // The internal database is kept separately.
    203 
    204             // touch the database file to show it is most recently used
    205             File file = new File(db.getPath());
    206             long now = System.currentTimeMillis();
    207             file.setLastModified(now);
    208 
    209             // delete least recently used databases if we are over the limit
    210             String[] databases = mContext.databaseList();
    211             int count = databases.length;
    212             int limit = MAX_EXTERNAL_DATABASES;
    213 
    214             // delete external databases that have not been used in the past two months
    215             long twoMonthsAgo = now - OBSOLETE_DATABASE_DB;
    216             for (int i = 0; i < databases.length; i++) {
    217                 File other = mContext.getDatabasePath(databases[i]);
    218                 if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) {
    219                     databases[i] = null;
    220                     count--;
    221                     if (file.equals(other)) {
    222                         // reduce limit to account for the existence of the database we
    223                         // are about to open, which we removed from the list.
    224                         limit--;
    225                     }
    226                 } else {
    227                     long time = other.lastModified();
    228                     if (time < twoMonthsAgo) {
    229                         if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[i]);
    230                         mContext.deleteDatabase(databases[i]);
    231                         databases[i] = null;
    232                         count--;
    233                     }
    234                 }
    235             }
    236 
    237             // delete least recently used databases until
    238             // we are no longer over the limit
    239             while (count > limit) {
    240                 int lruIndex = -1;
    241                 long lruTime = 0;
    242 
    243                 for (int i = 0; i < databases.length; i++) {
    244                     if (databases[i] != null) {
    245                         long time = mContext.getDatabasePath(databases[i]).lastModified();
    246                         if (lruTime == 0 || time < lruTime) {
    247                             lruIndex = i;
    248                             lruTime = time;
    249                         }
    250                     }
    251                 }
    252 
    253                 // delete least recently used database
    254                 if (lruIndex != -1) {
    255                     if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]);
    256                     mContext.deleteDatabase(databases[lruIndex]);
    257                     databases[lruIndex] = null;
    258                     count--;
    259                 }
    260             }
    261         }
    262     }
    263 
    264     @Override
    265     public boolean onCreate() {
    266         sArtistAlbumsMap.put(MediaStore.Audio.Albums._ID, "audio.album_id AS " +
    267                 MediaStore.Audio.Albums._ID);
    268         sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM, "album");
    269         sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_KEY, "album_key");
    270         sArtistAlbumsMap.put(MediaStore.Audio.Albums.FIRST_YEAR, "MIN(year) AS " +
    271                 MediaStore.Audio.Albums.FIRST_YEAR);
    272         sArtistAlbumsMap.put(MediaStore.Audio.Albums.LAST_YEAR, "MAX(year) AS " +
    273                 MediaStore.Audio.Albums.LAST_YEAR);
    274         sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST, "artist");
    275         sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_ID, "artist");
    276         sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_KEY, "artist_key");
    277         sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS, "count(*) AS " +
    278                 MediaStore.Audio.Albums.NUMBER_OF_SONGS);
    279         sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_ART, "album_art._data AS " +
    280                 MediaStore.Audio.Albums.ALBUM_ART);
    281 
    282         mSearchColsBasic[SEARCH_COLUMN_BASIC_TEXT2] =
    283                 mSearchColsBasic[SEARCH_COLUMN_BASIC_TEXT2].replaceAll(
    284                         "%1", getContext().getString(R.string.artist_label));
    285         mDatabases = new HashMap<String, DatabaseHelper>();
    286         attachVolume(INTERNAL_VOLUME);
    287 
    288         IntentFilter iFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
    289         iFilter.addDataScheme("file");
    290         getContext().registerReceiver(mUnmountReceiver, iFilter);
    291 
    292         // open external database if external storage is mounted
    293         String state = Environment.getExternalStorageState();
    294         if (Environment.MEDIA_MOUNTED.equals(state) ||
    295                 Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
    296             attachVolume(EXTERNAL_VOLUME);
    297         }
    298 
    299         HandlerThread ht = new HandlerThread("thumbs thread", Process.THREAD_PRIORITY_BACKGROUND);
    300         ht.start();
    301         mThumbHandler = new Handler(ht.getLooper()) {
    302             @Override
    303             public void handleMessage(Message msg) {
    304                 if (msg.what == IMAGE_THUMB) {
    305                     synchronized (mMediaThumbQueue) {
    306                         mCurrentThumbRequest = mMediaThumbQueue.poll();
    307                     }
    308                     if (mCurrentThumbRequest == null) {
    309                         Log.w(TAG, "Have message but no request?");
    310                     } else {
    311                         try {
    312                             File origFile = new File(mCurrentThumbRequest.mPath);
    313                             if (origFile.exists() && origFile.length() > 0) {
    314                                 mCurrentThumbRequest.execute();
    315                             } else {
    316                                 // original file hasn't been stored yet
    317                                 synchronized (mMediaThumbQueue) {
    318                                     Log.w(TAG, "original file hasn't been stored yet: " + mCurrentThumbRequest.mPath);
    319                                 }
    320                             }
    321                         } catch (IOException ex) {
    322                             Log.w(TAG, ex);
    323                         } catch (UnsupportedOperationException ex) {
    324                             // This could happen if we unplug the sd card during insert/update/delete
    325                             // See getDatabaseForUri.
    326                             Log.w(TAG, ex);
    327                         } finally {
    328                             synchronized (mCurrentThumbRequest) {
    329                                 mCurrentThumbRequest.mState = MediaThumbRequest.State.DONE;
    330                                 mCurrentThumbRequest.notifyAll();
    331                             }
    332                         }
    333                     }
    334                 } else if (msg.what == ALBUM_THUMB) {
    335                     ThumbData d;
    336                     synchronized (mThumbRequestStack) {
    337                         d = (ThumbData)mThumbRequestStack.pop();
    338                     }
    339 
    340                     makeThumbInternal(d);
    341                     synchronized (mPendingThumbs) {
    342                         mPendingThumbs.remove(d.path);
    343                     }
    344                 }
    345             }
    346         };
    347 
    348         return true;
    349     }
    350 
    351     /**
    352      * This method takes care of updating all the tables in the database to the
    353      * current version, creating them if necessary.
    354      * This method can only update databases at schema 63 or higher, which was
    355      * created August 1, 2008. Older database will be cleared and recreated.
    356      * @param db Database
    357      * @param internal True if this is the internal media database
    358      */
    359     private static void updateDatabase(SQLiteDatabase db, boolean internal,
    360             int fromVersion, int toVersion) {
    361 
    362         // sanity checks
    363         if (toVersion != DATABASE_VERSION) {
    364             Log.e(TAG, "Illegal update request. Got " + toVersion + ", expected " +
    365                     DATABASE_VERSION);
    366             throw new IllegalArgumentException();
    367         } else if (fromVersion > toVersion) {
    368             Log.e(TAG, "Illegal update request: can't downgrade from " + fromVersion +
    369                     " to " + toVersion + ". Did you forget to wipe data?");
    370             throw new IllegalArgumentException();
    371         }
    372 
    373         // Revisions 84-86 were a failed attempt at supporting the "album artist" id3 tag
    374         // We can't downgrade from those revisions, so start over.
    375         // (the initial change to do this was wrong, so now we actually need to start over
    376         // if the database version is 84-89)
    377         if (fromVersion < 63 || (fromVersion >= 84 && fromVersion <= 89)) {
    378             fromVersion = 63;
    379             // Drop everything and start over.
    380             Log.i(TAG, "Upgrading media database from version " +
    381                     fromVersion + " to " + toVersion + ", which will destroy all old data");
    382             db.execSQL("DROP TABLE IF EXISTS images");
    383             db.execSQL("DROP TRIGGER IF EXISTS images_cleanup");
    384             db.execSQL("DROP TABLE IF EXISTS thumbnails");
    385             db.execSQL("DROP TRIGGER IF EXISTS thumbnails_cleanup");
    386             db.execSQL("DROP TABLE IF EXISTS audio_meta");
    387             db.execSQL("DROP TABLE IF EXISTS artists");
    388             db.execSQL("DROP TABLE IF EXISTS albums");
    389             db.execSQL("DROP TABLE IF EXISTS album_art");
    390             db.execSQL("DROP VIEW IF EXISTS artist_info");
    391             db.execSQL("DROP VIEW IF EXISTS album_info");
    392             db.execSQL("DROP VIEW IF EXISTS artists_albums_map");
    393             db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup");
    394             db.execSQL("DROP TABLE IF EXISTS audio_genres");
    395             db.execSQL("DROP TABLE IF EXISTS audio_genres_map");
    396             db.execSQL("DROP TRIGGER IF EXISTS audio_genres_cleanup");
    397             db.execSQL("DROP TABLE IF EXISTS audio_playlists");
    398             db.execSQL("DROP TABLE IF EXISTS audio_playlists_map");
    399             db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup");
    400             db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup1");
    401             db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup2");
    402             db.execSQL("DROP TABLE IF EXISTS video");
    403             db.execSQL("DROP TRIGGER IF EXISTS video_cleanup");
    404 
    405             db.execSQL("CREATE TABLE IF NOT EXISTS images (" +
    406                     "_id INTEGER PRIMARY KEY," +
    407                     "_data TEXT," +
    408                     "_size INTEGER," +
    409                     "_display_name TEXT," +
    410                     "mime_type TEXT," +
    411                     "title TEXT," +
    412                     "date_added INTEGER," +
    413                     "date_modified INTEGER," +
    414                     "description TEXT," +
    415                     "picasa_id TEXT," +
    416                     "isprivate INTEGER," +
    417                     "latitude DOUBLE," +
    418                     "longitude DOUBLE," +
    419                     "datetaken INTEGER," +
    420                     "orientation INTEGER," +
    421                     "mini_thumb_magic INTEGER," +
    422                     "bucket_id TEXT," +
    423                     "bucket_display_name TEXT" +
    424                    ");");
    425 
    426             db.execSQL("CREATE INDEX IF NOT EXISTS mini_thumb_magic_index on images(mini_thumb_magic);");
    427 
    428             db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON images " +
    429                     "BEGIN " +
    430                         "DELETE FROM thumbnails WHERE image_id = old._id;" +
    431                         "SELECT _DELETE_FILE(old._data);" +
    432                     "END");
    433 
    434             // create image thumbnail table
    435             db.execSQL("CREATE TABLE IF NOT EXISTS thumbnails (" +
    436                        "_id INTEGER PRIMARY KEY," +
    437                        "_data TEXT," +
    438                        "image_id INTEGER," +
    439                        "kind INTEGER," +
    440                        "width INTEGER," +
    441                        "height INTEGER" +
    442                        ");");
    443 
    444             db.execSQL("CREATE INDEX IF NOT EXISTS image_id_index on thumbnails(image_id);");
    445 
    446             db.execSQL("CREATE TRIGGER IF NOT EXISTS thumbnails_cleanup DELETE ON thumbnails " +
    447                     "BEGIN " +
    448                         "SELECT _DELETE_FILE(old._data);" +
    449                     "END");
    450 
    451             // Contains meta data about audio files
    452             db.execSQL("CREATE TABLE IF NOT EXISTS audio_meta (" +
    453                        "_id INTEGER PRIMARY KEY," +
    454                        "_data TEXT UNIQUE NOT NULL," +
    455                        "_display_name TEXT," +
    456                        "_size INTEGER," +
    457                        "mime_type TEXT," +
    458                        "date_added INTEGER," +
    459                        "date_modified INTEGER," +
    460                        "title TEXT NOT NULL," +
    461                        "title_key TEXT NOT NULL," +
    462                        "duration INTEGER," +
    463                        "artist_id INTEGER," +
    464                        "composer TEXT," +
    465                        "album_id INTEGER," +
    466                        "track INTEGER," +    // track is an integer to allow proper sorting
    467                        "year INTEGER CHECK(year!=0)," +
    468                        "is_ringtone INTEGER," +
    469                        "is_music INTEGER," +
    470                        "is_alarm INTEGER," +
    471                        "is_notification INTEGER" +
    472                        ");");
    473 
    474             // Contains a sort/group "key" and the preferred display name for artists
    475             db.execSQL("CREATE TABLE IF NOT EXISTS artists (" +
    476                         "artist_id INTEGER PRIMARY KEY," +
    477                         "artist_key TEXT NOT NULL UNIQUE," +
    478                         "artist TEXT NOT NULL" +
    479                        ");");
    480 
    481             // Contains a sort/group "key" and the preferred display name for albums
    482             db.execSQL("CREATE TABLE IF NOT EXISTS albums (" +
    483                         "album_id INTEGER PRIMARY KEY," +
    484                         "album_key TEXT NOT NULL UNIQUE," +
    485                         "album TEXT NOT NULL" +
    486                        ");");
    487 
    488             db.execSQL("CREATE TABLE IF NOT EXISTS album_art (" +
    489                     "album_id INTEGER PRIMARY KEY," +
    490                     "_data TEXT" +
    491                    ");");
    492 
    493             recreateAudioView(db);
    494 
    495 
    496             // Provides some extra info about artists, like the number of tracks
    497             // and albums for this artist
    498             db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " +
    499                         "SELECT artist_id AS _id, artist, artist_key, " +
    500                         "COUNT(DISTINCT album) AS number_of_albums, " +
    501                         "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+
    502                         "GROUP BY artist_key;");
    503 
    504             // Provides extra info albums, such as the number of tracks
    505             db.execSQL("CREATE VIEW IF NOT EXISTS album_info AS " +
    506                     "SELECT audio.album_id AS _id, album, album_key, " +
    507                     "MIN(year) AS minyear, " +
    508                     "MAX(year) AS maxyear, artist, artist_id, artist_key, " +
    509                     "count(*) AS " + MediaStore.Audio.Albums.NUMBER_OF_SONGS +
    510                     ",album_art._data AS album_art" +
    511                     " FROM audio LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id" +
    512                     " WHERE is_music=1 GROUP BY audio.album_id;");
    513 
    514             // For a given artist_id, provides the album_id for albums on
    515             // which the artist appears.
    516             db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " +
    517                     "SELECT DISTINCT artist_id, album_id FROM audio_meta;");
    518 
    519             /*
    520              * Only external media volumes can handle genres, playlists, etc.
    521              */
    522             if (!internal) {
    523                 // Cleans up when an audio file is deleted
    524                 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON audio_meta " +
    525                            "BEGIN " +
    526                                "DELETE FROM audio_genres_map WHERE audio_id = old._id;" +
    527                                "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" +
    528                            "END");
    529 
    530                 // Contains audio genre definitions
    531                 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres (" +
    532                            "_id INTEGER PRIMARY KEY," +
    533                            "name TEXT NOT NULL" +
    534                            ");");
    535 
    536                 // Contiains mappings between audio genres and audio files
    537                 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres_map (" +
    538                            "_id INTEGER PRIMARY KEY," +
    539                            "audio_id INTEGER NOT NULL," +
    540                            "genre_id INTEGER NOT NULL" +
    541                            ");");
    542 
    543                 // Cleans up when an audio genre is delete
    544                 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_genres_cleanup DELETE ON audio_genres " +
    545                            "BEGIN " +
    546                                "DELETE FROM audio_genres_map WHERE genre_id = old._id;" +
    547                            "END");
    548 
    549                 // Contains audio playlist definitions
    550                 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists (" +
    551                            "_id INTEGER PRIMARY KEY," +
    552                            "_data TEXT," +  // _data is path for file based playlists, or null
    553                            "name TEXT NOT NULL," +
    554                            "date_added INTEGER," +
    555                            "date_modified INTEGER" +
    556                            ");");
    557 
    558                 // Contains mappings between audio playlists and audio files
    559                 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists_map (" +
    560                            "_id INTEGER PRIMARY KEY," +
    561                            "audio_id INTEGER NOT NULL," +
    562                            "playlist_id INTEGER NOT NULL," +
    563                            "play_order INTEGER NOT NULL" +
    564                            ");");
    565 
    566                 // Cleans up when an audio playlist is deleted
    567                 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON audio_playlists " +
    568                            "BEGIN " +
    569                                "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" +
    570                                "SELECT _DELETE_FILE(old._data);" +
    571                            "END");
    572 
    573                 // Cleans up album_art table entry when an album is deleted
    574                 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup1 DELETE ON albums " +
    575                         "BEGIN " +
    576                             "DELETE FROM album_art WHERE album_id = old.album_id;" +
    577                         "END");
    578 
    579                 // Cleans up album_art when an album is deleted
    580                 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup2 DELETE ON album_art " +
    581                         "BEGIN " +
    582                             "SELECT _DELETE_FILE(old._data);" +
    583                         "END");
    584             }
    585 
    586             // Contains meta data about video files
    587             db.execSQL("CREATE TABLE IF NOT EXISTS video (" +
    588                        "_id INTEGER PRIMARY KEY," +
    589                        "_data TEXT NOT NULL," +
    590                        "_display_name TEXT," +
    591                        "_size INTEGER," +
    592                        "mime_type TEXT," +
    593                        "date_added INTEGER," +
    594                        "date_modified INTEGER," +
    595                        "title TEXT," +
    596                        "duration INTEGER," +
    597                        "artist TEXT," +
    598                        "album TEXT," +
    599                        "resolution TEXT," +
    600                        "description TEXT," +
    601                        "isprivate INTEGER," +   // for YouTube videos
    602                        "tags TEXT," +           // for YouTube videos
    603                        "category TEXT," +       // for YouTube videos
    604                        "language TEXT," +       // for YouTube videos
    605                        "mini_thumb_data TEXT," +
    606                        "latitude DOUBLE," +
    607                        "longitude DOUBLE," +
    608                        "datetaken INTEGER," +
    609                        "mini_thumb_magic INTEGER" +
    610                        ");");
    611 
    612             db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON video " +
    613                     "BEGIN " +
    614                         "SELECT _DELETE_FILE(old._data);" +
    615                     "END");
    616         }
    617 
    618         // At this point the database is at least at schema version 63 (it was
    619         // either created at version 63 by the code above, or was already at
    620         // version 63 or later)
    621 
    622         if (fromVersion < 64) {
    623             // create the index that updates the database to schema version 64
    624             db.execSQL("CREATE INDEX IF NOT EXISTS sort_index on images(datetaken ASC, _id ASC);");
    625         }
    626 
    627         /*
    628          *  Android 1.0 shipped with database version 64
    629          */
    630 
    631         if (fromVersion < 65) {
    632             // create the index that updates the database to schema version 65
    633             db.execSQL("CREATE INDEX IF NOT EXISTS titlekey_index on audio_meta(title_key);");
    634         }
    635 
    636         // In version 66, originally we updateBucketNames(db, "images"),
    637         // but we need to do it in version 89 and therefore save the update here.
    638 
    639         if (fromVersion < 67) {
    640             // create the indices that update the database to schema version 67
    641             db.execSQL("CREATE INDEX IF NOT EXISTS albumkey_index on albums(album_key);");
    642             db.execSQL("CREATE INDEX IF NOT EXISTS artistkey_index on artists(artist_key);");
    643         }
    644 
    645         if (fromVersion < 68) {
    646             // Create bucket_id and bucket_display_name columns for the video table.
    647             db.execSQL("ALTER TABLE video ADD COLUMN bucket_id TEXT;");
    648             db.execSQL("ALTER TABLE video ADD COLUMN bucket_display_name TEXT");
    649 
    650             // In version 68, originally we updateBucketNames(db, "video"),
    651             // but we need to do it in version 89 and therefore save the update here.
    652         }
    653 
    654         if (fromVersion < 69) {
    655             updateDisplayName(db, "images");
    656         }
    657 
    658         if (fromVersion < 70) {
    659             // Create bookmark column for the video table.
    660             db.execSQL("ALTER TABLE video ADD COLUMN bookmark INTEGER;");
    661         }
    662 
    663         if (fromVersion < 71) {
    664             // There is no change to the database schema, however a code change
    665             // fixed parsing of metadata for certain files bought from the
    666             // iTunes music store, so we want to rescan files that might need it.
    667             // We do this by clearing the modification date in the database for
    668             // those files, so that the media scanner will see them as updated
    669             // and rescan them.
    670             db.execSQL("UPDATE audio_meta SET date_modified=0 WHERE _id IN (" +
    671                     "SELECT _id FROM audio where mime_type='audio/mp4' AND " +
    672                     "artist='" + MediaStore.UNKNOWN_STRING + "' AND " +
    673                     "album='" + MediaStore.UNKNOWN_STRING + "'" +
    674                     ");");
    675         }
    676 
    677         if (fromVersion < 72) {
    678             // Create is_podcast and bookmark columns for the audio table.
    679             db.execSQL("ALTER TABLE audio_meta ADD COLUMN is_podcast INTEGER;");
    680             db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE _data LIKE '%/podcasts/%';");
    681             db.execSQL("UPDATE audio_meta SET is_music=0 WHERE is_podcast=1" +
    682                     " AND _data NOT LIKE '%/music/%';");
    683             db.execSQL("ALTER TABLE audio_meta ADD COLUMN bookmark INTEGER;");
    684 
    685             // New columns added to tables aren't visible in views on those tables
    686             // without opening and closing the database (or using the 'vacuum' command,
    687             // which we can't do here because all this code runs inside a transaction).
    688             // To work around this, we drop and recreate the affected view and trigger.
    689             recreateAudioView(db);
    690         }
    691 
    692         /*
    693          *  Android 1.5 shipped with database version 72
    694          */
    695 
    696         if (fromVersion < 73) {
    697             // There is no change to the database schema, but we now do case insensitive
    698             // matching of folder names when determining whether something is music, a
    699             // ringtone, podcast, etc, so we might need to reclassify some files.
    700             db.execSQL("UPDATE audio_meta SET is_music=1 WHERE is_music=0 AND " +
    701                     "_data LIKE '%/music/%';");
    702             db.execSQL("UPDATE audio_meta SET is_ringtone=1 WHERE is_ringtone=0 AND " +
    703                     "_data LIKE '%/ringtones/%';");
    704             db.execSQL("UPDATE audio_meta SET is_notification=1 WHERE is_notification=0 AND " +
    705                     "_data LIKE '%/notifications/%';");
    706             db.execSQL("UPDATE audio_meta SET is_alarm=1 WHERE is_alarm=0 AND " +
    707                     "_data LIKE '%/alarms/%';");
    708             db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE is_podcast=0 AND " +
    709                     "_data LIKE '%/podcasts/%';");
    710         }
    711 
    712         if (fromVersion < 74) {
    713             // This view is used instead of the audio view by the union below, to force
    714             // sqlite to use the title_key index. This greatly reduces memory usage
    715             // (no separate copy pass needed for sorting, which could cause errors on
    716             // large datasets) and improves speed (by about 35% on a large dataset)
    717             db.execSQL("CREATE VIEW IF NOT EXISTS searchhelpertitle AS SELECT * FROM audio " +
    718                     "ORDER BY title_key;");
    719 
    720             db.execSQL("CREATE VIEW IF NOT EXISTS search AS " +
    721                     "SELECT _id," +
    722                     "'artist' AS mime_type," +
    723                     "artist," +
    724                     "NULL AS album," +
    725                     "NULL AS title," +
    726                     "artist AS text1," +
    727                     "NULL AS text2," +
    728                     "number_of_albums AS data1," +
    729                     "number_of_tracks AS data2," +
    730                     "artist_key AS match," +
    731                     "'content://media/external/audio/artists/'||_id AS suggest_intent_data," +
    732                     "1 AS grouporder " +
    733                     "FROM artist_info WHERE (artist!='" + MediaStore.UNKNOWN_STRING + "') " +
    734                 "UNION ALL " +
    735                     "SELECT _id," +
    736                     "'album' AS mime_type," +
    737                     "artist," +
    738                     "album," +
    739                     "NULL AS title," +
    740                     "album AS text1," +
    741                     "artist AS text2," +
    742                     "NULL AS data1," +
    743                     "NULL AS data2," +
    744                     "artist_key||' '||album_key AS match," +
    745                     "'content://media/external/audio/albums/'||_id AS suggest_intent_data," +
    746                     "2 AS grouporder " +
    747                     "FROM album_info WHERE (album!='" + MediaStore.UNKNOWN_STRING + "') " +
    748                 "UNION ALL " +
    749                     "SELECT searchhelpertitle._id AS _id," +
    750                     "mime_type," +
    751                     "artist," +
    752                     "album," +
    753                     "title," +
    754                     "title AS text1," +
    755                     "artist AS text2," +
    756                     "NULL AS data1," +
    757                     "NULL AS data2," +
    758                     "artist_key||' '||album_key||' '||title_key AS match," +
    759                     "'content://media/external/audio/media/'||searchhelpertitle._id AS " +
    760                     "suggest_intent_data," +
    761                     "3 AS grouporder " +
    762                     "FROM searchhelpertitle WHERE (title != '') "
    763                     );
    764         }
    765 
    766         if (fromVersion < 75) {
    767             // Force a rescan of the audio entries so we can apply the new logic to
    768             // distinguish same-named albums.
    769             db.execSQL("UPDATE audio_meta SET date_modified=0;");
    770             db.execSQL("DELETE FROM albums");
    771         }
    772 
    773         if (fromVersion < 76) {
    774             // We now ignore double quotes when building the key, so we have to remove all of them
    775             // from existing keys.
    776             db.execSQL("UPDATE audio_meta SET title_key=" +
    777                     "REPLACE(title_key,x'081D08C29F081D',x'081D') " +
    778                     "WHERE title_key LIKE '%'||x'081D08C29F081D'||'%';");
    779             db.execSQL("UPDATE albums SET album_key=" +
    780                     "REPLACE(album_key,x'081D08C29F081D',x'081D') " +
    781                     "WHERE album_key LIKE '%'||x'081D08C29F081D'||'%';");
    782             db.execSQL("UPDATE artists SET artist_key=" +
    783                     "REPLACE(artist_key,x'081D08C29F081D',x'081D') " +
    784                     "WHERE artist_key LIKE '%'||x'081D08C29F081D'||'%';");
    785         }
    786 
    787         /*
    788          *  Android 1.6 shipped with database version 76
    789          */
    790 
    791         if (fromVersion < 77) {
    792             // create video thumbnail table
    793             db.execSQL("CREATE TABLE IF NOT EXISTS videothumbnails (" +
    794                     "_id INTEGER PRIMARY KEY," +
    795                     "_data TEXT," +
    796                     "video_id INTEGER," +
    797                     "kind INTEGER," +
    798                     "width INTEGER," +
    799                     "height INTEGER" +
    800                     ");");
    801 
    802             db.execSQL("CREATE INDEX IF NOT EXISTS video_id_index on videothumbnails(video_id);");
    803 
    804             db.execSQL("CREATE TRIGGER IF NOT EXISTS videothumbnails_cleanup DELETE ON videothumbnails " +
    805                     "BEGIN " +
    806                         "SELECT _DELETE_FILE(old._data);" +
    807                     "END");
    808         }
    809 
    810         /*
    811          *  Android 2.0 and 2.0.1 shipped with database version 77
    812          */
    813 
    814         if (fromVersion < 78) {
    815             // Force a rescan of the video entries so we can update
    816             // latest changed DATE_TAKEN units (in milliseconds).
    817             db.execSQL("UPDATE video SET date_modified=0;");
    818         }
    819 
    820         /*
    821          *  Android 2.1 shipped with database version 78
    822          */
    823 
    824         if (fromVersion < 79) {
    825             // move /sdcard/albumthumbs to
    826             // /sdcard/Android/data/com.android.providers.media/albumthumbs,
    827             // and update the database accordingly
    828 
    829             String storageroot = Environment.getExternalStorageDirectory().getAbsolutePath();
    830             String oldthumbspath = storageroot + "/albumthumbs";
    831             String newthumbspath = storageroot + "/" + ALBUM_THUMB_FOLDER;
    832             File thumbsfolder = new File(oldthumbspath);
    833             if (thumbsfolder.exists()) {
    834                 // move folder to its new location
    835                 File newthumbsfolder = new File(newthumbspath);
    836                 newthumbsfolder.getParentFile().mkdirs();
    837                 if(thumbsfolder.renameTo(newthumbsfolder)) {
    838                     // update the database
    839                     db.execSQL("UPDATE album_art SET _data=REPLACE(_data, '" +
    840                             oldthumbspath + "','" + newthumbspath + "');");
    841                 }
    842             }
    843         }
    844 
    845         if (fromVersion < 80) {
    846             // Force rescan of image entries to update DATE_TAKEN as UTC timestamp.
    847             db.execSQL("UPDATE images SET date_modified=0;");
    848         }
    849 
    850         if (fromVersion < 81 && !internal) {
    851             // Delete entries starting with /mnt/sdcard. This is for the benefit
    852             // of users running builds between 2.0.1 and 2.1 final only, since
    853             // users updating from 2.0 or earlier will not have such entries.
    854 
    855             // First we need to update the _data fields in the affected tables, since
    856             // otherwise deleting the entries will also delete the underlying files
    857             // (via a trigger), and we want to keep them.
    858             db.execSQL("UPDATE audio_playlists SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';");
    859             db.execSQL("UPDATE images SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';");
    860             db.execSQL("UPDATE video SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';");
    861             db.execSQL("UPDATE videothumbnails SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';");
    862             db.execSQL("UPDATE thumbnails SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';");
    863             db.execSQL("UPDATE album_art SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';");
    864             db.execSQL("UPDATE audio_meta SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';");
    865             // Once the paths have been renamed, we can safely delete the entries
    866             db.execSQL("DELETE FROM audio_playlists WHERE _data IS '////';");
    867             db.execSQL("DELETE FROM images WHERE _data IS '////';");
    868             db.execSQL("DELETE FROM video WHERE _data IS '////';");
    869             db.execSQL("DELETE FROM videothumbnails WHERE _data IS '////';");
    870             db.execSQL("DELETE FROM thumbnails WHERE _data IS '////';");
    871             db.execSQL("DELETE FROM audio_meta WHERE _data  IS '////';");
    872             db.execSQL("DELETE FROM album_art WHERE _data  IS '////';");
    873 
    874             // rename existing entries starting with /sdcard to /mnt/sdcard
    875             db.execSQL("UPDATE audio_meta" +
    876                     " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';");
    877             db.execSQL("UPDATE audio_playlists" +
    878                     " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';");
    879             db.execSQL("UPDATE images" +
    880                     " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';");
    881             db.execSQL("UPDATE video" +
    882                     " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';");
    883             db.execSQL("UPDATE videothumbnails" +
    884                     " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';");
    885             db.execSQL("UPDATE thumbnails" +
    886                     " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';");
    887             db.execSQL("UPDATE album_art" +
    888                     " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';");
    889 
    890             // Delete albums and artists, then clear the modification time on songs, which
    891             // will cause the media scanner to rescan everything, rebuilding the artist and
    892             // album tables along the way, while preserving playlists.
    893             // We need this rescan because ICU also changed, and now generates different
    894             // collation keys
    895             db.execSQL("DELETE from albums");
    896             db.execSQL("DELETE from artists");
    897             db.execSQL("UPDATE audio_meta SET date_modified=0;");
    898         }
    899 
    900         if (fromVersion < 82) {
    901             // recreate this view with the correct "group by" specifier
    902             db.execSQL("DROP VIEW IF EXISTS artist_info");
    903             db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " +
    904                         "SELECT artist_id AS _id, artist, artist_key, " +
    905                         "COUNT(DISTINCT album_key) AS number_of_albums, " +
    906                         "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+
    907                         "GROUP BY artist_key;");
    908         }
    909 
    910         /* we skipped over version 83, and reverted versions 84, 85 and 86 */
    911 
    912         if (fromVersion < 87) {
    913             // The fastscroll thumb needs an index on the strings being displayed,
    914             // otherwise the queries it does to determine the correct position
    915             // becomes really inefficient
    916             db.execSQL("CREATE INDEX IF NOT EXISTS title_idx on audio_meta(title);");
    917             db.execSQL("CREATE INDEX IF NOT EXISTS artist_idx on artists(artist);");
    918             db.execSQL("CREATE INDEX IF NOT EXISTS album_idx on albums(album);");
    919         }
    920 
    921         if (fromVersion < 88) {
    922             // Clean up a few more things from versions 84/85/86, and recreate
    923             // the few things worth keeping from those changes.
    924             db.execSQL("DROP TRIGGER IF EXISTS albums_update1;");
    925             db.execSQL("DROP TRIGGER IF EXISTS albums_update2;");
    926             db.execSQL("DROP TRIGGER IF EXISTS albums_update3;");
    927             db.execSQL("DROP TRIGGER IF EXISTS albums_update4;");
    928             db.execSQL("DROP TRIGGER IF EXISTS artist_update1;");
    929             db.execSQL("DROP TRIGGER IF EXISTS artist_update2;");
    930             db.execSQL("DROP TRIGGER IF EXISTS artist_update3;");
    931             db.execSQL("DROP TRIGGER IF EXISTS artist_update4;");
    932             db.execSQL("DROP VIEw IF EXISTS album_artists;");
    933             db.execSQL("CREATE INDEX IF NOT EXISTS album_id_idx on audio_meta(album_id);");
    934             db.execSQL("CREATE INDEX IF NOT EXISTS artist_id_idx on audio_meta(artist_id);");
    935             // For a given artist_id, provides the album_id for albums on
    936             // which the artist appears.
    937             db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " +
    938                     "SELECT DISTINCT artist_id, album_id FROM audio_meta;");
    939         }
    940 
    941         if (fromVersion < 89) {
    942             updateBucketNames(db, "images");
    943             updateBucketNames(db, "video");
    944         }
    945         sanityCheck(db, fromVersion);
    946     }
    947 
    948     /**
    949      * Perform a simple sanity check on the database. Currently this tests
    950      * whether all the _data entries in audio_meta are unique
    951      */
    952     private static void sanityCheck(SQLiteDatabase db, int fromVersion) {
    953         Cursor c1 = db.query("audio_meta", new String[] {"count(*)"},
    954                 null, null, null, null, null);
    955         Cursor c2 = db.query("audio_meta", new String[] {"count(distinct _data)"},
    956                 null, null, null, null, null);
    957         c1.moveToFirst();
    958         c2.moveToFirst();
    959         int num1 = c1.getInt(0);
    960         int num2 = c2.getInt(0);
    961         c1.close();
    962         c2.close();
    963         if (num1 != num2) {
    964             Log.e(TAG, "audio_meta._data column is not unique while upgrading" +
    965                     " from schema " +fromVersion + " : " + num1 +"/" + num2);
    966             // Delete all audio_meta rows so they will be rebuilt by the media scanner
    967             db.execSQL("DELETE FROM audio_meta;");
    968         }
    969     }
    970 
    971     private static void recreateAudioView(SQLiteDatabase db) {
    972         // Provides a unified audio/artist/album info view.
    973         // Note that views are read-only, so we define a trigger to allow deletes.
    974         db.execSQL("DROP VIEW IF EXISTS audio");
    975         db.execSQL("DROP TRIGGER IF EXISTS audio_delete");
    976         db.execSQL("CREATE VIEW IF NOT EXISTS audio as SELECT * FROM audio_meta " +
    977                     "LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id " +
    978                     "LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id;");
    979 
    980         db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " +
    981                 "BEGIN " +
    982                     "DELETE from audio_meta where _id=old._id;" +
    983                     "DELETE from audio_playlists_map where audio_id=old._id;" +
    984                     "DELETE from audio_genres_map where audio_id=old._id;" +
    985                 "END");
    986     }
    987 
    988     /**
    989      * Iterate through the rows of a table in a database, ensuring that the bucket_id and
    990      * bucket_display_name columns are correct.
    991      * @param db
    992      * @param tableName
    993      */
    994     private static void updateBucketNames(SQLiteDatabase db, String tableName) {
    995         // Rebuild the bucket_display_name column using the natural case rather than lower case.
    996         db.beginTransaction();
    997         try {
    998             String[] columns = {BaseColumns._ID, MediaColumns.DATA};
    999             Cursor cursor = db.query(tableName, columns, null, null, null, null, null);
   1000             try {
   1001                 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID);
   1002                 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA);
   1003                 while (cursor.moveToNext()) {
   1004                     String data = cursor.getString(dataColumnIndex);
   1005                     ContentValues values = new ContentValues();
   1006                     computeBucketValues(data, values);
   1007                     int rowId = cursor.getInt(idColumnIndex);
   1008                     db.update(tableName, values, "_id=" + rowId, null);
   1009                 }
   1010             } finally {
   1011                 cursor.close();
   1012             }
   1013             db.setTransactionSuccessful();
   1014         } finally {
   1015             db.endTransaction();
   1016         }
   1017     }
   1018 
   1019     /**
   1020      * Iterate through the rows of a table in a database, ensuring that the
   1021      * display name column has a value.
   1022      * @param db
   1023      * @param tableName
   1024      */
   1025     private static void updateDisplayName(SQLiteDatabase db, String tableName) {
   1026         // Fill in default values for null displayName values
   1027         db.beginTransaction();
   1028         try {
   1029             String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME};
   1030             Cursor cursor = db.query(tableName, columns, null, null, null, null, null);
   1031             try {
   1032                 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID);
   1033                 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA);
   1034                 final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME);
   1035                 ContentValues values = new ContentValues();
   1036                 while (cursor.moveToNext()) {
   1037                     String displayName = cursor.getString(displayNameIndex);
   1038                     if (displayName == null) {
   1039                         String data = cursor.getString(dataColumnIndex);
   1040                         values.clear();
   1041                         computeDisplayName(data, values);
   1042                         int rowId = cursor.getInt(idColumnIndex);
   1043                         db.update(tableName, values, "_id=" + rowId, null);
   1044                     }
   1045                 }
   1046             } finally {
   1047                 cursor.close();
   1048             }
   1049             db.setTransactionSuccessful();
   1050         } finally {
   1051             db.endTransaction();
   1052         }
   1053     }
   1054     /**
   1055      * @param data The input path
   1056      * @param values the content values, where the bucked id name and bucket display name are updated.
   1057      *
   1058      */
   1059 
   1060     private static void computeBucketValues(String data, ContentValues values) {
   1061         File parentFile = new File(data).getParentFile();
   1062         if (parentFile == null) {
   1063             parentFile = new File("/");
   1064         }
   1065 
   1066         // Lowercase the path for hashing. This avoids duplicate buckets if the
   1067         // filepath case is changed externally.
   1068         // Keep the original case for display.
   1069         String path = parentFile.toString().toLowerCase();
   1070         String name = parentFile.getName();
   1071 
   1072         // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the
   1073         // same for both images and video. However, for backwards-compatibility reasons
   1074         // there is no common base class. We use the ImageColumns version here
   1075         values.put(ImageColumns.BUCKET_ID, path.hashCode());
   1076         values.put(ImageColumns.BUCKET_DISPLAY_NAME, name);
   1077     }
   1078 
   1079     /**
   1080      * @param data The input path
   1081      * @param values the content values, where the display name is updated.
   1082      *
   1083      */
   1084     private static void computeDisplayName(String data, ContentValues values) {
   1085         String s = (data == null ? "" : data.toString());
   1086         int idx = s.lastIndexOf('/');
   1087         if (idx >= 0) {
   1088             s = s.substring(idx + 1);
   1089         }
   1090         values.put("_display_name", s);
   1091     }
   1092 
   1093     /**
   1094      * Copy taken time from date_modified if we lost the original value (e.g. after factory reset)
   1095      * This works for both video and image tables.
   1096      *
   1097      * @param values the content values, where taken time is updated.
   1098      */
   1099     private static void computeTakenTime(ContentValues values) {
   1100         if (! values.containsKey(Images.Media.DATE_TAKEN)) {
   1101             // This only happens when MediaScanner finds an image file that doesn't have any useful
   1102             // reference to get this value. (e.g. GPSTimeStamp)
   1103             Long lastModified = values.getAsLong(MediaColumns.DATE_MODIFIED);
   1104             if (lastModified != null) {
   1105                 values.put(Images.Media.DATE_TAKEN, lastModified * 1000);
   1106             }
   1107         }
   1108     }
   1109 
   1110     /**
   1111      * This method blocks until thumbnail is ready.
   1112      *
   1113      * @param thumbUri
   1114      * @return
   1115      */
   1116     private boolean waitForThumbnailReady(Uri origUri) {
   1117         Cursor c = this.query(origUri, new String[] { ImageColumns._ID, ImageColumns.DATA,
   1118                 ImageColumns.MINI_THUMB_MAGIC}, null, null, null);
   1119         if (c == null) return false;
   1120 
   1121         boolean result = false;
   1122 
   1123         if (c.moveToFirst()) {
   1124             long id = c.getLong(0);
   1125             String path = c.getString(1);
   1126             long magic = c.getLong(2);
   1127 
   1128             MediaThumbRequest req = requestMediaThumbnail(path, origUri,
   1129                     MediaThumbRequest.PRIORITY_HIGH, magic);
   1130             if (req == null) {
   1131                 return false;
   1132             }
   1133             synchronized (req) {
   1134                 try {
   1135                     while (req.mState == MediaThumbRequest.State.WAIT) {
   1136                         req.wait();
   1137                     }
   1138                 } catch (InterruptedException e) {
   1139                     Log.w(TAG, e);
   1140                 }
   1141                 if (req.mState == MediaThumbRequest.State.DONE) {
   1142                     result = true;
   1143                 }
   1144             }
   1145         }
   1146         c.close();
   1147 
   1148         return result;
   1149     }
   1150 
   1151     private boolean matchThumbRequest(MediaThumbRequest req, int pid, long id, long gid,
   1152             boolean isVideo) {
   1153         boolean cancelAllOrigId = (id == -1);
   1154         boolean cancelAllGroupId = (gid == -1);
   1155         return (req.mCallingPid == pid) &&
   1156                 (cancelAllGroupId || req.mGroupId == gid) &&
   1157                 (cancelAllOrigId || req.mOrigId == id) &&
   1158                 (req.mIsVideo == isVideo);
   1159     }
   1160 
   1161     private boolean queryThumbnail(SQLiteQueryBuilder qb, Uri uri, String table,
   1162             String column, boolean hasThumbnailId) {
   1163         qb.setTables(table);
   1164         if (hasThumbnailId) {
   1165             // For uri dispatched to this method, the 4th path segment is always
   1166             // the thumbnail id.
   1167             qb.appendWhere("_id = " + uri.getPathSegments().get(3));
   1168             // client already knows which thumbnail it wants, bypass it.
   1169             return true;
   1170         }
   1171         String origId = uri.getQueryParameter("orig_id");
   1172         // We can't query ready_flag unless we know original id
   1173         if (origId == null) {
   1174             // this could be thumbnail query for other purpose, bypass it.
   1175             return true;
   1176         }
   1177 
   1178         boolean needBlocking = "1".equals(uri.getQueryParameter("blocking"));
   1179         boolean cancelRequest = "1".equals(uri.getQueryParameter("cancel"));
   1180         Uri origUri = uri.buildUpon().encodedPath(
   1181                 uri.getPath().replaceFirst("thumbnails", "media"))
   1182                 .appendPath(origId).build();
   1183 
   1184         if (needBlocking && !waitForThumbnailReady(origUri)) {
   1185             Log.w(TAG, "original media doesn't exist or it's canceled.");
   1186             return false;
   1187         } else if (cancelRequest) {
   1188             String groupId = uri.getQueryParameter("group_id");
   1189             boolean isVideo = "video".equals(uri.getPathSegments().get(1));
   1190             int pid = Binder.getCallingPid();
   1191             long id = -1;
   1192             long gid = -1;
   1193 
   1194             try {
   1195                 id = Long.parseLong(origId);
   1196                 gid = Long.parseLong(groupId);
   1197             } catch (NumberFormatException ex) {
   1198                 // invalid cancel request
   1199                 return false;
   1200             }
   1201 
   1202             synchronized (mMediaThumbQueue) {
   1203                 if (mCurrentThumbRequest != null &&
   1204                         matchThumbRequest(mCurrentThumbRequest, pid, id, gid, isVideo)) {
   1205                     synchronized (mCurrentThumbRequest) {
   1206                         mCurrentThumbRequest.mState = MediaThumbRequest.State.CANCEL;
   1207                         mCurrentThumbRequest.notifyAll();
   1208                     }
   1209                 }
   1210                 for (MediaThumbRequest mtq : mMediaThumbQueue) {
   1211                     if (matchThumbRequest(mtq, pid, id, gid, isVideo)) {
   1212                         synchronized (mtq) {
   1213                             mtq.mState = MediaThumbRequest.State.CANCEL;
   1214                             mtq.notifyAll();
   1215                         }
   1216 
   1217                         mMediaThumbQueue.remove(mtq);
   1218                     }
   1219                 }
   1220             }
   1221         }
   1222 
   1223         if (origId != null) {
   1224             qb.appendWhere(column + " = " + origId);
   1225         }
   1226         return true;
   1227     }
   1228     @SuppressWarnings("fallthrough")
   1229     @Override
   1230     public Cursor query(Uri uri, String[] projectionIn, String selection,
   1231             String[] selectionArgs, String sort) {
   1232         int table = URI_MATCHER.match(uri);
   1233 
   1234         // Log.v(TAG, "query: uri="+uri+", selection="+selection);
   1235         // handle MEDIA_SCANNER before calling getDatabaseForUri()
   1236         if (table == MEDIA_SCANNER) {
   1237             if (mMediaScannerVolume == null) {
   1238                 return null;
   1239             } else {
   1240                 // create a cursor to return volume currently being scanned by the media scanner
   1241                 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME});
   1242                 c.addRow(new String[] {mMediaScannerVolume});
   1243                 return c;
   1244             }
   1245         }
   1246 
   1247         // Used temporarily (until we have unique media IDs) to get an identifier
   1248         // for the current sd card, so that the music app doesn't have to use the
   1249         // non-public getFatVolumeId method
   1250         if (table == FS_ID) {
   1251             MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
   1252             c.addRow(new Integer[] {mVolumeId});
   1253             return c;
   1254         }
   1255 
   1256         String groupBy = null;
   1257         DatabaseHelper database = getDatabaseForUri(uri);
   1258         if (database == null) {
   1259             return null;
   1260         }
   1261         SQLiteDatabase db = database.getReadableDatabase();
   1262         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   1263         String limit = uri.getQueryParameter("limit");
   1264         boolean hasThumbnailId = false;
   1265 
   1266         switch (table) {
   1267             case IMAGES_MEDIA:
   1268                 qb.setTables("images");
   1269                 if (uri.getQueryParameter("distinct") != null)
   1270                     qb.setDistinct(true);
   1271 
   1272                 // set the project map so that data dir is prepended to _data.
   1273                 //qb.setProjectionMap(mImagesProjectionMap, true);
   1274                 break;
   1275 
   1276             case IMAGES_MEDIA_ID:
   1277                 qb.setTables("images");
   1278                 if (uri.getQueryParameter("distinct") != null)
   1279                     qb.setDistinct(true);
   1280 
   1281                 // set the project map so that data dir is prepended to _data.
   1282                 //qb.setProjectionMap(mImagesProjectionMap, true);
   1283                 qb.appendWhere("_id = " + uri.getPathSegments().get(3));
   1284                 break;
   1285 
   1286             case IMAGES_THUMBNAILS_ID:
   1287                 hasThumbnailId = true;
   1288             case IMAGES_THUMBNAILS:
   1289                 if (!queryThumbnail(qb, uri, "thumbnails", "image_id", hasThumbnailId)) {
   1290                     return null;
   1291                 }
   1292                 break;
   1293 
   1294             case AUDIO_MEDIA:
   1295                 if (projectionIn != null && projectionIn.length == 1 &&  selectionArgs == null
   1296                         && (selection == null || selection.equalsIgnoreCase("is_music=1")
   1297                           || selection.equalsIgnoreCase("is_podcast=1") )
   1298                         && projectionIn[0].equalsIgnoreCase("count(*)") ) {
   1299                     //Log.i("@@@@", "taking fast path for counting songs");
   1300                     qb.setTables("audio_meta");
   1301                 } else {
   1302                     qb.setTables("audio");
   1303                 }
   1304                 break;
   1305 
   1306             case AUDIO_MEDIA_ID:
   1307                 qb.setTables("audio");
   1308                 qb.appendWhere("_id=" + uri.getPathSegments().get(3));
   1309                 break;
   1310 
   1311             case AUDIO_MEDIA_ID_GENRES:
   1312                 qb.setTables("audio_genres");
   1313                 qb.appendWhere("_id IN (SELECT genre_id FROM " +
   1314                         "audio_genres_map WHERE audio_id = " +
   1315                         uri.getPathSegments().get(3) + ")");
   1316                 break;
   1317 
   1318             case AUDIO_MEDIA_ID_GENRES_ID:
   1319                 qb.setTables("audio_genres");
   1320                 qb.appendWhere("_id=" + uri.getPathSegments().get(5));
   1321                 break;
   1322 
   1323             case AUDIO_MEDIA_ID_PLAYLISTS:
   1324                 qb.setTables("audio_playlists");
   1325                 qb.appendWhere("_id IN (SELECT playlist_id FROM " +
   1326                         "audio_playlists_map WHERE audio_id = " +
   1327                         uri.getPathSegments().get(3) + ")");
   1328                 break;
   1329 
   1330             case AUDIO_MEDIA_ID_PLAYLISTS_ID:
   1331                 qb.setTables("audio_playlists");
   1332                 qb.appendWhere("_id=" + uri.getPathSegments().get(5));
   1333                 break;
   1334 
   1335             case AUDIO_GENRES:
   1336                 qb.setTables("audio_genres");
   1337                 break;
   1338 
   1339             case AUDIO_GENRES_ID:
   1340                 qb.setTables("audio_genres");
   1341                 qb.appendWhere("_id=" + uri.getPathSegments().get(3));
   1342                 break;
   1343 
   1344             case AUDIO_GENRES_ID_MEMBERS:
   1345                 qb.setTables("audio");
   1346                 qb.appendWhere("_id IN (SELECT audio_id FROM " +
   1347                         "audio_genres_map WHERE genre_id = " +
   1348                         uri.getPathSegments().get(3) + ")");
   1349                 break;
   1350 
   1351             case AUDIO_GENRES_ID_MEMBERS_ID:
   1352                 qb.setTables("audio");
   1353                 qb.appendWhere("_id=" + uri.getPathSegments().get(5));
   1354                 break;
   1355 
   1356             case AUDIO_PLAYLISTS:
   1357                 qb.setTables("audio_playlists");
   1358                 break;
   1359 
   1360             case AUDIO_PLAYLISTS_ID:
   1361                 qb.setTables("audio_playlists");
   1362                 qb.appendWhere("_id=" + uri.getPathSegments().get(3));
   1363                 break;
   1364 
   1365             case AUDIO_PLAYLISTS_ID_MEMBERS:
   1366                 if (projectionIn != null) {
   1367                     for (int i = 0; i < projectionIn.length; i++) {
   1368                         if (projectionIn[i].equals("_id")) {
   1369                             projectionIn[i] = "audio_playlists_map._id AS _id";
   1370                         }
   1371                     }
   1372                 }
   1373                 qb.setTables("audio_playlists_map, audio");
   1374                 qb.appendWhere("audio._id = audio_id AND playlist_id = "
   1375                         + uri.getPathSegments().get(3));
   1376                 break;
   1377 
   1378             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
   1379                 qb.setTables("audio");
   1380                 qb.appendWhere("_id=" + uri.getPathSegments().get(5));
   1381                 break;
   1382 
   1383             case VIDEO_MEDIA:
   1384                 qb.setTables("video");
   1385                 break;
   1386 
   1387             case VIDEO_MEDIA_ID:
   1388                 qb.setTables("video");
   1389                 qb.appendWhere("_id=" + uri.getPathSegments().get(3));
   1390                 break;
   1391 
   1392             case VIDEO_THUMBNAILS_ID:
   1393                 hasThumbnailId = true;
   1394             case VIDEO_THUMBNAILS:
   1395                 if (!queryThumbnail(qb, uri, "videothumbnails", "video_id", hasThumbnailId)) {
   1396                     return null;
   1397                 }
   1398                 break;
   1399 
   1400             case AUDIO_ARTISTS:
   1401                 if (projectionIn != null && projectionIn.length == 1 &&  selectionArgs == null
   1402                         && (selection == null || selection.length() == 0)
   1403                         && projectionIn[0].equalsIgnoreCase("count(*)") ) {
   1404                     //Log.i("@@@@", "taking fast path for counting artists");
   1405                     qb.setTables("audio_meta");
   1406                     projectionIn[0] = "count(distinct artist_id)";
   1407                     qb.appendWhere("is_music=1");
   1408                 } else {
   1409                     qb.setTables("artist_info");
   1410                 }
   1411                 break;
   1412 
   1413             case AUDIO_ARTISTS_ID:
   1414                 qb.setTables("artist_info");
   1415                 qb.appendWhere("_id=" + uri.getPathSegments().get(3));
   1416                 break;
   1417 
   1418             case AUDIO_ARTISTS_ID_ALBUMS:
   1419                 String aid = uri.getPathSegments().get(3);
   1420                 qb.setTables("audio LEFT OUTER JOIN album_art ON" +
   1421                         " audio.album_id=album_art.album_id");
   1422                 qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " +
   1423                         "artists_albums_map WHERE artist_id = " +
   1424                          aid + ")");
   1425                 groupBy = "audio.album_id";
   1426                 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST,
   1427                         "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " +
   1428                         MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST);
   1429                 qb.setProjectionMap(sArtistAlbumsMap);
   1430                 break;
   1431 
   1432             case AUDIO_ALBUMS:
   1433                 if (projectionIn != null && projectionIn.length == 1 &&  selectionArgs == null
   1434                         && (selection == null || selection.length() == 0)
   1435                         && projectionIn[0].equalsIgnoreCase("count(*)") ) {
   1436                     //Log.i("@@@@", "taking fast path for counting albums");
   1437                     qb.setTables("audio_meta");
   1438                     projectionIn[0] = "count(distinct album_id)";
   1439                     qb.appendWhere("is_music=1");
   1440                 } else {
   1441                     qb.setTables("album_info");
   1442                 }
   1443                 break;
   1444 
   1445             case AUDIO_ALBUMS_ID:
   1446                 qb.setTables("album_info");
   1447                 qb.appendWhere("_id=" + uri.getPathSegments().get(3));
   1448                 break;
   1449 
   1450             case AUDIO_ALBUMART_ID:
   1451                 qb.setTables("album_art");
   1452                 qb.appendWhere("album_id=" + uri.getPathSegments().get(3));
   1453                 break;
   1454 
   1455             case AUDIO_SEARCH_LEGACY:
   1456                 Log.w(TAG, "Legacy media search Uri used. Please update your code.");
   1457                 // fall through
   1458             case AUDIO_SEARCH_FANCY:
   1459             case AUDIO_SEARCH_BASIC:
   1460                 return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort,
   1461                         table, limit);
   1462 
   1463             default:
   1464                 throw new IllegalStateException("Unknown URL: " + uri.toString());
   1465         }
   1466 
   1467         // Log.v(TAG, "query = "+ qb.buildQuery(projectionIn, selection, selectionArgs, groupBy, null, sort, limit));
   1468         Cursor c = qb.query(db, projectionIn, selection,
   1469                 selectionArgs, groupBy, null, sort, limit);
   1470 
   1471         if (c != null) {
   1472             c.setNotificationUri(getContext().getContentResolver(), uri);
   1473         }
   1474 
   1475         return c;
   1476     }
   1477 
   1478     private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb,
   1479             Uri uri, String[] projectionIn, String selection,
   1480             String[] selectionArgs, String sort, int mode,
   1481             String limit) {
   1482 
   1483         String mSearchString = uri.getPath().endsWith("/") ? "" : uri.getLastPathSegment();
   1484         mSearchString = mSearchString.replaceAll("  ", " ").trim().toLowerCase();
   1485 
   1486         String [] searchWords = mSearchString.length() > 0 ?
   1487                 mSearchString.split(" ") : new String[0];
   1488         String [] wildcardWords = new String[searchWords.length];
   1489         Collator col = Collator.getInstance();
   1490         col.setStrength(Collator.PRIMARY);
   1491         int len = searchWords.length;
   1492         for (int i = 0; i < len; i++) {
   1493             // Because we match on individual words here, we need to remove words
   1494             // like 'a' and 'the' that aren't part of the keys.
   1495             wildcardWords[i] =
   1496                 (searchWords[i].equals("a") || searchWords[i].equals("an") ||
   1497                         searchWords[i].equals("the")) ? "%" :
   1498                 '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
   1499         }
   1500 
   1501         String where = "";
   1502         for (int i = 0; i < searchWords.length; i++) {
   1503             if (i == 0) {
   1504                 where = "match LIKE ?";
   1505             } else {
   1506                 where += " AND match LIKE ?";
   1507             }
   1508         }
   1509 
   1510         qb.setTables("search");
   1511         String [] cols;
   1512         if (mode == AUDIO_SEARCH_FANCY) {
   1513             cols = mSearchColsFancy;
   1514         } else if (mode == AUDIO_SEARCH_BASIC) {
   1515             cols = mSearchColsBasic;
   1516         } else {
   1517             cols = mSearchColsLegacy;
   1518         }
   1519         return qb.query(db, cols, where, wildcardWords, null, null, null, limit);
   1520     }
   1521 
   1522     @Override
   1523     public String getType(Uri url)
   1524     {
   1525         switch (URI_MATCHER.match(url)) {
   1526             case IMAGES_MEDIA_ID:
   1527             case AUDIO_MEDIA_ID:
   1528             case AUDIO_GENRES_ID_MEMBERS_ID:
   1529             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
   1530             case VIDEO_MEDIA_ID:
   1531                 Cursor c = null;
   1532                 try {
   1533                     c = query(url, MIME_TYPE_PROJECTION, null, null, null);
   1534                     if (c != null && c.getCount() == 1) {
   1535                         c.moveToFirst();
   1536                         String mimeType = c.getString(1);
   1537                         c.deactivate();
   1538                         return mimeType;
   1539                     }
   1540                 } finally {
   1541                     if (c != null) {
   1542                         c.close();
   1543                     }
   1544                 }
   1545                 break;
   1546 
   1547             case IMAGES_MEDIA:
   1548             case IMAGES_THUMBNAILS:
   1549                 return Images.Media.CONTENT_TYPE;
   1550             case IMAGES_THUMBNAILS_ID:
   1551                 return "image/jpeg";
   1552 
   1553             case AUDIO_MEDIA:
   1554             case AUDIO_GENRES_ID_MEMBERS:
   1555             case AUDIO_PLAYLISTS_ID_MEMBERS:
   1556                 return Audio.Media.CONTENT_TYPE;
   1557 
   1558             case AUDIO_GENRES:
   1559             case AUDIO_MEDIA_ID_GENRES:
   1560                 return Audio.Genres.CONTENT_TYPE;
   1561             case AUDIO_GENRES_ID:
   1562             case AUDIO_MEDIA_ID_GENRES_ID:
   1563                 return Audio.Genres.ENTRY_CONTENT_TYPE;
   1564             case AUDIO_PLAYLISTS:
   1565             case AUDIO_MEDIA_ID_PLAYLISTS:
   1566                 return Audio.Playlists.CONTENT_TYPE;
   1567             case AUDIO_PLAYLISTS_ID:
   1568             case AUDIO_MEDIA_ID_PLAYLISTS_ID:
   1569                 return Audio.Playlists.ENTRY_CONTENT_TYPE;
   1570 
   1571             case VIDEO_MEDIA:
   1572                 return Video.Media.CONTENT_TYPE;
   1573         }
   1574         throw new IllegalStateException("Unknown URL");
   1575     }
   1576 
   1577     /**
   1578      * Ensures there is a file in the _data column of values, if one isn't
   1579      * present a new file is created.
   1580      *
   1581      * @param initialValues the values passed to insert by the caller
   1582      * @return the new values
   1583      */
   1584     private ContentValues ensureFile(boolean internal, ContentValues initialValues,
   1585             String preferredExtension, String directoryName) {
   1586         ContentValues values;
   1587         String file = initialValues.getAsString("_data");
   1588         if (TextUtils.isEmpty(file)) {
   1589             file = generateFileName(internal, preferredExtension, directoryName);
   1590             values = new ContentValues(initialValues);
   1591             values.put("_data", file);
   1592         } else {
   1593             values = initialValues;
   1594         }
   1595 
   1596         if (!ensureFileExists(file)) {
   1597             throw new IllegalStateException("Unable to create new file: " + file);
   1598         }
   1599         return values;
   1600     }
   1601 
   1602     @Override
   1603     public int bulkInsert(Uri uri, ContentValues values[]) {
   1604         int match = URI_MATCHER.match(uri);
   1605         if (match == VOLUMES) {
   1606             return super.bulkInsert(uri, values);
   1607         }
   1608         DatabaseHelper database = getDatabaseForUri(uri);
   1609         if (database == null) {
   1610             throw new UnsupportedOperationException(
   1611                     "Unknown URI: " + uri);
   1612         }
   1613         SQLiteDatabase db = database.getWritableDatabase();
   1614 
   1615         if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) {
   1616             return playlistBulkInsert(db, uri, values);
   1617         }
   1618 
   1619         db.beginTransaction();
   1620         int numInserted = 0;
   1621         try {
   1622             int len = values.length;
   1623             for (int i = 0; i < len; i++) {
   1624                 insertInternal(uri, values[i]);
   1625             }
   1626             numInserted = len;
   1627             db.setTransactionSuccessful();
   1628         } finally {
   1629             db.endTransaction();
   1630         }
   1631         getContext().getContentResolver().notifyChange(uri, null);
   1632         return numInserted;
   1633     }
   1634 
   1635     @Override
   1636     public Uri insert(Uri uri, ContentValues initialValues)
   1637     {
   1638         Uri newUri = insertInternal(uri, initialValues);
   1639         if (newUri != null) {
   1640             getContext().getContentResolver().notifyChange(uri, null);
   1641         }
   1642         return newUri;
   1643     }
   1644 
   1645     private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) {
   1646         DatabaseUtils.InsertHelper helper =
   1647             new DatabaseUtils.InsertHelper(db, "audio_playlists_map");
   1648         int audioidcolidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
   1649         int playlistididx = helper.getColumnIndex(Audio.Playlists.Members.PLAYLIST_ID);
   1650         int playorderidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
   1651         long playlistId = Long.parseLong(uri.getPathSegments().get(3));
   1652 
   1653         db.beginTransaction();
   1654         int numInserted = 0;
   1655         try {
   1656             int len = values.length;
   1657             for (int i = 0; i < len; i++) {
   1658                 helper.prepareForInsert();
   1659                 // getting the raw Object and converting it long ourselves saves
   1660                 // an allocation (the alternative is ContentValues.getAsLong, which
   1661                 // returns a Long object)
   1662                 long audioid = ((Number) values[i].get(
   1663                         MediaStore.Audio.Playlists.Members.AUDIO_ID)).longValue();
   1664                 helper.bind(audioidcolidx, audioid);
   1665                 helper.bind(playlistididx, playlistId);
   1666                 // convert to int ourselves to save an allocation.
   1667                 int playorder = ((Number) values[i].get(
   1668                         MediaStore.Audio.Playlists.Members.PLAY_ORDER)).intValue();
   1669                 helper.bind(playorderidx, playorder);
   1670                 helper.execute();
   1671             }
   1672             numInserted = len;
   1673             db.setTransactionSuccessful();
   1674         } finally {
   1675             db.endTransaction();
   1676             helper.close();
   1677         }
   1678         getContext().getContentResolver().notifyChange(uri, null);
   1679         return numInserted;
   1680     }
   1681 
   1682     private Uri insertInternal(Uri uri, ContentValues initialValues) {
   1683         long rowId;
   1684         int match = URI_MATCHER.match(uri);
   1685 
   1686         // Log.v(TAG, "insertInternal: "+uri+", initValues="+initialValues);
   1687         // handle MEDIA_SCANNER before calling getDatabaseForUri()
   1688         if (match == MEDIA_SCANNER) {
   1689             mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);
   1690             return MediaStore.getMediaScannerUri();
   1691         }
   1692 
   1693         Uri newUri = null;
   1694         DatabaseHelper database = getDatabaseForUri(uri);
   1695         if (database == null && match != VOLUMES) {
   1696             throw new UnsupportedOperationException(
   1697                     "Unknown URI: " + uri);
   1698         }
   1699         SQLiteDatabase db = (match == VOLUMES ? null : database.getWritableDatabase());
   1700 
   1701         if (initialValues == null) {
   1702             initialValues = new ContentValues();
   1703         }
   1704 
   1705         switch (match) {
   1706             case IMAGES_MEDIA: {
   1707                 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/Camera");
   1708 
   1709                 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
   1710                 String data = values.getAsString(MediaColumns.DATA);
   1711                 if (! values.containsKey(MediaColumns.DISPLAY_NAME)) {
   1712                     computeDisplayName(data, values);
   1713                 }
   1714                 computeBucketValues(data, values);
   1715                 computeTakenTime(values);
   1716                 rowId = db.insert("images", "name", values);
   1717 
   1718                 if (rowId > 0) {
   1719                     newUri = ContentUris.withAppendedId(
   1720                             Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId);
   1721                     requestMediaThumbnail(data, newUri, MediaThumbRequest.PRIORITY_NORMAL, 0);
   1722                 }
   1723                 break;
   1724             }
   1725 
   1726             // This will be triggered by requestMediaThumbnail (see getThumbnailUri)
   1727             case IMAGES_THUMBNAILS: {
   1728                 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg",
   1729                         "DCIM/.thumbnails");
   1730                 rowId = db.insert("thumbnails", "name", values);
   1731                 if (rowId > 0) {
   1732                     newUri = ContentUris.withAppendedId(Images.Thumbnails.
   1733                             getContentUri(uri.getPathSegments().get(0)), rowId);
   1734                 }
   1735                 break;
   1736             }
   1737 
   1738             // This is currently only used by MICRO_KIND video thumbnail (see getThumbnailUri)
   1739             case VIDEO_THUMBNAILS: {
   1740                 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg",
   1741                         "DCIM/.thumbnails");
   1742                 rowId = db.insert("videothumbnails", "name", values);
   1743                 if (rowId > 0) {
   1744                     newUri = ContentUris.withAppendedId(Video.Thumbnails.
   1745                             getContentUri(uri.getPathSegments().get(0)), rowId);
   1746                 }
   1747                 break;
   1748             }
   1749 
   1750             case AUDIO_MEDIA: {
   1751                 // SQLite Views are read-only, so we need to deconstruct this
   1752                 // insert and do inserts into the underlying tables.
   1753                 // If doing this here turns out to be a performance bottleneck,
   1754                 // consider moving this to native code and using triggers on
   1755                 // the view.
   1756                 ContentValues values = new ContentValues(initialValues);
   1757 
   1758                 // TODO Remove this and actually store the album_artist in the
   1759                 // database. For now this is here so the media scanner can start
   1760                 // sending us the album_artist, even though it's not in the db yet.
   1761                 values.remove(MediaStore.Audio.Media.ALBUM_ARTIST);
   1762 
   1763                 // Insert the artist into the artist table and remove it from
   1764                 // the input values
   1765                 Object so = values.get("artist");
   1766                 String s = (so == null ? "" : so.toString());
   1767                 values.remove("artist");
   1768                 long artistRowId;
   1769                 HashMap<String, Long> artistCache = database.mArtistCache;
   1770                 String path = values.getAsString("_data");
   1771                 synchronized(artistCache) {
   1772                     Long temp = artistCache.get(s);
   1773                     if (temp == null) {
   1774                         artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist",
   1775                                 s, s, path, 0, null, artistCache, uri);
   1776                     } else {
   1777                         artistRowId = temp.longValue();
   1778                     }
   1779                 }
   1780                 String artist = s;
   1781 
   1782                 // Do the same for the album field
   1783                 so = values.get("album");
   1784                 s = (so == null ? "" : so.toString());
   1785                 values.remove("album");
   1786                 long albumRowId;
   1787                 HashMap<String, Long> albumCache = database.mAlbumCache;
   1788                 synchronized(albumCache) {
   1789                     int albumhash = path.substring(0, path.lastIndexOf('/')).hashCode();
   1790                     String cacheName = s + albumhash;
   1791                     Long temp = albumCache.get(cacheName);
   1792                     if (temp == null) {
   1793                         albumRowId = getKeyIdForName(db, "albums", "album_key", "album",
   1794                                 s, cacheName, path, albumhash, artist, albumCache, uri);
   1795                     } else {
   1796                         albumRowId = temp;
   1797                     }
   1798                 }
   1799 
   1800                 values.put("artist_id", Integer.toString((int)artistRowId));
   1801                 values.put("album_id", Integer.toString((int)albumRowId));
   1802                 so = values.getAsString("title");
   1803                 s = (so == null ? "" : so.toString());
   1804                 values.put("title_key", MediaStore.Audio.keyFor(s));
   1805                 // do a final trim of the title, in case it started with the special
   1806                 // "sort first" character (ascii \001)
   1807                 values.remove("title");
   1808                 values.put("title", s.trim());
   1809 
   1810                 computeDisplayName(values.getAsString("_data"), values);
   1811                 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
   1812 
   1813                 rowId = db.insert("audio_meta", "duration", values);
   1814                 if (rowId > 0) {
   1815                     newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId);
   1816                 }
   1817                 break;
   1818             }
   1819 
   1820             case AUDIO_MEDIA_ID_GENRES: {
   1821                 Long audioId = Long.parseLong(uri.getPathSegments().get(2));
   1822                 ContentValues values = new ContentValues(initialValues);
   1823                 values.put(Audio.Genres.Members.AUDIO_ID, audioId);
   1824                 rowId = db.insert("audio_genres_map", "genre_id", values);
   1825                 if (rowId > 0) {
   1826                     newUri = ContentUris.withAppendedId(uri, rowId);
   1827                 }
   1828                 break;
   1829             }
   1830 
   1831             case AUDIO_MEDIA_ID_PLAYLISTS: {
   1832                 Long audioId = Long.parseLong(uri.getPathSegments().get(2));
   1833                 ContentValues values = new ContentValues(initialValues);
   1834                 values.put(Audio.Playlists.Members.AUDIO_ID, audioId);
   1835                 rowId = db.insert("audio_playlists_map", "playlist_id",
   1836                         values);
   1837                 if (rowId > 0) {
   1838                     newUri = ContentUris.withAppendedId(uri, rowId);
   1839                 }
   1840                 break;
   1841             }
   1842 
   1843             case AUDIO_GENRES: {
   1844                 rowId = db.insert("audio_genres", "audio_id", initialValues);
   1845                 if (rowId > 0) {
   1846                     newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId);
   1847                 }
   1848                 break;
   1849             }
   1850 
   1851             case AUDIO_GENRES_ID_MEMBERS: {
   1852                 Long genreId = Long.parseLong(uri.getPathSegments().get(3));
   1853                 ContentValues values = new ContentValues(initialValues);
   1854                 values.put(Audio.Genres.Members.GENRE_ID, genreId);
   1855                 rowId = db.insert("audio_genres_map", "genre_id", values);
   1856                 if (rowId > 0) {
   1857                     newUri = ContentUris.withAppendedId(uri, rowId);
   1858                 }
   1859                 break;
   1860             }
   1861 
   1862             case AUDIO_PLAYLISTS: {
   1863                 ContentValues values = new ContentValues(initialValues);
   1864                 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
   1865                 rowId = db.insert("audio_playlists", "name", initialValues);
   1866                 if (rowId > 0) {
   1867                     newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId);
   1868                 }
   1869                 break;
   1870             }
   1871 
   1872             case AUDIO_PLAYLISTS_ID:
   1873             case AUDIO_PLAYLISTS_ID_MEMBERS: {
   1874                 Long playlistId = Long.parseLong(uri.getPathSegments().get(3));
   1875                 ContentValues values = new ContentValues(initialValues);
   1876                 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId);
   1877                 rowId = db.insert("audio_playlists_map", "playlist_id", values);
   1878                 if (rowId > 0) {
   1879                     newUri = ContentUris.withAppendedId(uri, rowId);
   1880                 }
   1881                 break;
   1882             }
   1883 
   1884             case VIDEO_MEDIA: {
   1885                 ContentValues values = ensureFile(database.mInternal, initialValues, ".3gp", "video");
   1886                 String data = values.getAsString("_data");
   1887                 computeDisplayName(data, values);
   1888                 computeBucketValues(data, values);
   1889                 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
   1890                 computeTakenTime(values);
   1891                 rowId = db.insert("video", "artist", values);
   1892                 if (rowId > 0) {
   1893                     newUri = ContentUris.withAppendedId(Video.Media.getContentUri(
   1894                             uri.getPathSegments().get(0)), rowId);
   1895                     requestMediaThumbnail(data, newUri, MediaThumbRequest.PRIORITY_NORMAL, 0);
   1896                 }
   1897                 break;
   1898             }
   1899 
   1900             case AUDIO_ALBUMART:
   1901                 if (database.mInternal) {
   1902                     throw new UnsupportedOperationException("no internal album art allowed");
   1903                 }
   1904                 ContentValues values = null;
   1905                 try {
   1906                     values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER);
   1907                 } catch (IllegalStateException ex) {
   1908                     // probably no more room to store albumthumbs
   1909                     values = initialValues;
   1910                 }
   1911                 rowId = db.insert("album_art", "_data", values);
   1912                 if (rowId > 0) {
   1913                     newUri = ContentUris.withAppendedId(uri, rowId);
   1914                 }
   1915                 break;
   1916 
   1917             case VOLUMES:
   1918                 return attachVolume(initialValues.getAsString("name"));
   1919 
   1920             default:
   1921                 throw new UnsupportedOperationException("Invalid URI " + uri);
   1922         }
   1923 
   1924         return newUri;
   1925     }
   1926 
   1927     @Override
   1928     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
   1929                 throws OperationApplicationException {
   1930 
   1931         // The operations array provides no overall information about the URI(s) being operated
   1932         // on, so begin a transaction for ALL of the databases.
   1933         DatabaseHelper ihelper = getDatabaseForUri(MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
   1934         DatabaseHelper ehelper = getDatabaseForUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
   1935         SQLiteDatabase idb = ihelper.getWritableDatabase();
   1936         idb.beginTransaction();
   1937         SQLiteDatabase edb = null;
   1938         if (ehelper != null) {
   1939             edb = ehelper.getWritableDatabase();
   1940             edb.beginTransaction();
   1941         }
   1942         try {
   1943             ContentProviderResult[] result = super.applyBatch(operations);
   1944             idb.setTransactionSuccessful();
   1945             if (edb != null) {
   1946                 edb.setTransactionSuccessful();
   1947             }
   1948             // Rather than sending targeted change notifications for every Uri
   1949             // affected by the batch operation, just invalidate the entire internal
   1950             // and external name space.
   1951             ContentResolver res = getContext().getContentResolver();
   1952             res.notifyChange(Uri.parse("content://media/"), null);
   1953             return result;
   1954         } finally {
   1955             idb.endTransaction();
   1956             if (edb != null) {
   1957                 edb.endTransaction();
   1958             }
   1959         }
   1960     }
   1961 
   1962 
   1963     private MediaThumbRequest requestMediaThumbnail(String path, Uri uri, int priority, long magic) {
   1964         synchronized (mMediaThumbQueue) {
   1965             MediaThumbRequest req = null;
   1966             try {
   1967                 req = new MediaThumbRequest(
   1968                         getContext().getContentResolver(), path, uri, priority, magic);
   1969                 mMediaThumbQueue.add(req);
   1970                 // Trigger the handler.
   1971                 Message msg = mThumbHandler.obtainMessage(IMAGE_THUMB);
   1972                 msg.sendToTarget();
   1973             } catch (Throwable t) {
   1974                 Log.w(TAG, t);
   1975             }
   1976             return req;
   1977         }
   1978     }
   1979 
   1980     private String generateFileName(boolean internal, String preferredExtension, String directoryName)
   1981     {
   1982         // create a random file
   1983         String name = String.valueOf(System.currentTimeMillis());
   1984 
   1985         if (internal) {
   1986             throw new UnsupportedOperationException("Writing to internal storage is not supported.");
   1987 //            return Environment.getDataDirectory()
   1988 //                + "/" + directoryName + "/" + name + preferredExtension;
   1989         } else {
   1990             return Environment.getExternalStorageDirectory()
   1991                 + "/" + directoryName + "/" + name + preferredExtension;
   1992         }
   1993     }
   1994 
   1995     private boolean ensureFileExists(String path) {
   1996         File file = new File(path);
   1997         if (file.exists()) {
   1998             return true;
   1999         } else {
   2000             // we will not attempt to create the first directory in the path
   2001             // (for example, do not create /sdcard if the SD card is not mounted)
   2002             int secondSlash = path.indexOf('/', 1);
   2003             if (secondSlash < 1) return false;
   2004             String directoryPath = path.substring(0, secondSlash);
   2005             File directory = new File(directoryPath);
   2006             if (!directory.exists())
   2007                 return false;
   2008             file.getParentFile().mkdirs();
   2009             try {
   2010                 return file.createNewFile();
   2011             } catch(IOException ioe) {
   2012                 Log.e(TAG, "File creation failed", ioe);
   2013             }
   2014             return false;
   2015         }
   2016     }
   2017 
   2018     private static final class GetTableAndWhereOutParameter {
   2019         public String table;
   2020         public String where;
   2021     }
   2022 
   2023     static final GetTableAndWhereOutParameter sGetTableAndWhereParam =
   2024             new GetTableAndWhereOutParameter();
   2025 
   2026     private void getTableAndWhere(Uri uri, int match, String userWhere,
   2027             GetTableAndWhereOutParameter out) {
   2028         String where = null;
   2029         switch (match) {
   2030             case IMAGES_MEDIA:
   2031                 out.table = "images";
   2032                 break;
   2033 
   2034             case IMAGES_MEDIA_ID:
   2035                 out.table = "images";
   2036                 where = "_id = " + uri.getPathSegments().get(3);
   2037                 break;
   2038 
   2039             case IMAGES_THUMBNAILS_ID:
   2040                 where = "_id=" + uri.getPathSegments().get(3);
   2041             case IMAGES_THUMBNAILS:
   2042                 out.table = "thumbnails";
   2043                 break;
   2044 
   2045             case AUDIO_MEDIA:
   2046                 out.table = "audio";
   2047                 break;
   2048 
   2049             case AUDIO_MEDIA_ID:
   2050                 out.table = "audio";
   2051                 where = "_id=" + uri.getPathSegments().get(3);
   2052                 break;
   2053 
   2054             case AUDIO_MEDIA_ID_GENRES:
   2055                 out.table = "audio_genres";
   2056                 where = "audio_id=" + uri.getPathSegments().get(3);
   2057                 break;
   2058 
   2059             case AUDIO_MEDIA_ID_GENRES_ID:
   2060                 out.table = "audio_genres";
   2061                 where = "audio_id=" + uri.getPathSegments().get(3) +
   2062                         " AND genre_id=" + uri.getPathSegments().get(5);
   2063                break;
   2064 
   2065             case AUDIO_MEDIA_ID_PLAYLISTS:
   2066                 out.table = "audio_playlists";
   2067                 where = "audio_id=" + uri.getPathSegments().get(3);
   2068                 break;
   2069 
   2070             case AUDIO_MEDIA_ID_PLAYLISTS_ID:
   2071                 out.table = "audio_playlists";
   2072                 where = "audio_id=" + uri.getPathSegments().get(3) +
   2073                         " AND playlists_id=" + uri.getPathSegments().get(5);
   2074                 break;
   2075 
   2076             case AUDIO_GENRES:
   2077                 out.table = "audio_genres";
   2078                 break;
   2079 
   2080             case AUDIO_GENRES_ID:
   2081                 out.table = "audio_genres";
   2082                 where = "_id=" + uri.getPathSegments().get(3);
   2083                 break;
   2084 
   2085             case AUDIO_GENRES_ID_MEMBERS:
   2086                 out.table = "audio_genres";
   2087                 where = "genre_id=" + uri.getPathSegments().get(3);
   2088                 break;
   2089 
   2090             case AUDIO_GENRES_ID_MEMBERS_ID:
   2091                 out.table = "audio_genres";
   2092                 where = "genre_id=" + uri.getPathSegments().get(3) +
   2093                         " AND audio_id =" + uri.getPathSegments().get(5);
   2094                 break;
   2095 
   2096             case AUDIO_PLAYLISTS:
   2097                 out.table = "audio_playlists";
   2098                 break;
   2099 
   2100             case AUDIO_PLAYLISTS_ID:
   2101                 out.table = "audio_playlists";
   2102                 where = "_id=" + uri.getPathSegments().get(3);
   2103                 break;
   2104 
   2105             case AUDIO_PLAYLISTS_ID_MEMBERS:
   2106                 out.table = "audio_playlists_map";
   2107                 where = "playlist_id=" + uri.getPathSegments().get(3);
   2108                 break;
   2109 
   2110             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
   2111                 out.table = "audio_playlists_map";
   2112                 where = "playlist_id=" + uri.getPathSegments().get(3) +
   2113                         " AND _id=" + uri.getPathSegments().get(5);
   2114                 break;
   2115 
   2116             case AUDIO_ALBUMART_ID:
   2117                 out.table = "album_art";
   2118                 where = "album_id=" + uri.getPathSegments().get(3);
   2119                 break;
   2120 
   2121             case VIDEO_MEDIA:
   2122                 out.table = "video";
   2123                 break;
   2124 
   2125             case VIDEO_MEDIA_ID:
   2126                 out.table = "video";
   2127                 where = "_id=" + uri.getPathSegments().get(3);
   2128                 break;
   2129 
   2130             case VIDEO_THUMBNAILS_ID:
   2131                 where = "_id=" + uri.getPathSegments().get(3);
   2132             case VIDEO_THUMBNAILS:
   2133                 out.table = "videothumbnails";
   2134                 break;
   2135 
   2136             default:
   2137                 throw new UnsupportedOperationException(
   2138                         "Unknown or unsupported URL: " + uri.toString());
   2139         }
   2140 
   2141         // Add in the user requested WHERE clause, if needed
   2142         if (!TextUtils.isEmpty(userWhere)) {
   2143             if (!TextUtils.isEmpty(where)) {
   2144                 out.where = where + " AND (" + userWhere + ")";
   2145             } else {
   2146                 out.where = userWhere;
   2147             }
   2148         } else {
   2149             out.where = where;
   2150         }
   2151     }
   2152 
   2153     @Override
   2154     public int delete(Uri uri, String userWhere, String[] whereArgs) {
   2155         int count;
   2156         int match = URI_MATCHER.match(uri);
   2157 
   2158         // handle MEDIA_SCANNER before calling getDatabaseForUri()
   2159         if (match == MEDIA_SCANNER) {
   2160             if (mMediaScannerVolume == null) {
   2161                 return 0;
   2162             }
   2163             mMediaScannerVolume = null;
   2164             return 1;
   2165         }
   2166 
   2167         if (match != VOLUMES_ID) {
   2168             DatabaseHelper database = getDatabaseForUri(uri);
   2169             if (database == null) {
   2170                 throw new UnsupportedOperationException(
   2171                         "Unknown URI: " + uri);
   2172             }
   2173             SQLiteDatabase db = database.getWritableDatabase();
   2174 
   2175             synchronized (sGetTableAndWhereParam) {
   2176                 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam);
   2177                 switch (match) {
   2178                     case AUDIO_MEDIA:
   2179                     case AUDIO_MEDIA_ID:
   2180                         count = db.delete("audio_meta",
   2181                                 sGetTableAndWhereParam.where, whereArgs);
   2182                         break;
   2183                     default:
   2184                         count = db.delete(sGetTableAndWhereParam.table,
   2185                                 sGetTableAndWhereParam.where, whereArgs);
   2186                         break;
   2187                 }
   2188                 getContext().getContentResolver().notifyChange(uri, null);
   2189             }
   2190         } else {
   2191             detachVolume(uri);
   2192             count = 1;
   2193         }
   2194 
   2195         return count;
   2196     }
   2197 
   2198     @Override
   2199     public int update(Uri uri, ContentValues initialValues, String userWhere,
   2200             String[] whereArgs) {
   2201         int count;
   2202         // Log.v(TAG, "update for uri="+uri+", initValues="+initialValues);
   2203         int match = URI_MATCHER.match(uri);
   2204         DatabaseHelper database = getDatabaseForUri(uri);
   2205         if (database == null) {
   2206             throw new UnsupportedOperationException(
   2207                     "Unknown URI: " + uri);
   2208         }
   2209         SQLiteDatabase db = database.getWritableDatabase();
   2210 
   2211         synchronized (sGetTableAndWhereParam) {
   2212             getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam);
   2213 
   2214             switch (match) {
   2215                 case AUDIO_MEDIA:
   2216                 case AUDIO_MEDIA_ID:
   2217                     {
   2218                         ContentValues values = new ContentValues(initialValues);
   2219                         // TODO Remove this and actually store the album_artist in the
   2220                         // database. For now this is here so the media scanner can start
   2221                         // sending us the album_artist, even though it's not in the db yet.
   2222                         values.remove(MediaStore.Audio.Media.ALBUM_ARTIST);
   2223 
   2224                         // Insert the artist into the artist table and remove it from
   2225                         // the input values
   2226                         String artist = values.getAsString("artist");
   2227                         values.remove("artist");
   2228                         if (artist != null) {
   2229                             long artistRowId;
   2230                             HashMap<String, Long> artistCache = database.mArtistCache;
   2231                             synchronized(artistCache) {
   2232                                 Long temp = artistCache.get(artist);
   2233                                 if (temp == null) {
   2234                                     artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist",
   2235                                             artist, artist, null, 0, null, artistCache, uri);
   2236                                 } else {
   2237                                     artistRowId = temp.longValue();
   2238                                 }
   2239                             }
   2240                             values.put("artist_id", Integer.toString((int)artistRowId));
   2241                         }
   2242 
   2243                         // Do the same for the album field.
   2244                         String so = values.getAsString("album");
   2245                         values.remove("album");
   2246                         if (so != null) {
   2247                             String path = values.getAsString("_data");
   2248                             int albumHash = 0;
   2249                             if (path == null) {
   2250                                 // If the path is null, we don't have a hash for the file in question.
   2251                                 Log.w(TAG, "Update without specified path.");
   2252                             } else {
   2253                                 albumHash = path.substring(0, path.lastIndexOf('/')).hashCode();
   2254                             }
   2255                             String s = so.toString();
   2256                             long albumRowId;
   2257                             HashMap<String, Long> albumCache = database.mAlbumCache;
   2258                             synchronized(albumCache) {
   2259                                 String cacheName = s + albumHash;
   2260                                 Long temp = albumCache.get(cacheName);
   2261                                 if (temp == null) {
   2262                                     albumRowId = getKeyIdForName(db, "albums", "album_key", "album",
   2263                                             s, cacheName, path, albumHash, artist, albumCache, uri);
   2264                                 } else {
   2265                                     albumRowId = temp.longValue();
   2266                                 }
   2267                             }
   2268                             values.put("album_id", Integer.toString((int)albumRowId));
   2269                         }
   2270 
   2271                         // don't allow the title_key field to be updated directly
   2272                         values.remove("title_key");
   2273                         // If the title field is modified, update the title_key
   2274                         so = values.getAsString("title");
   2275                         if (so != null) {
   2276                             String s = so.toString();
   2277                             values.put("title_key", MediaStore.Audio.keyFor(s));
   2278                             // do a final trim of the title, in case it started with the special
   2279                             // "sort first" character (ascii \001)
   2280                             values.remove("title");
   2281                             values.put("title", s.trim());
   2282                         }
   2283 
   2284                         count = db.update("audio_meta", values, sGetTableAndWhereParam.where,
   2285                                 whereArgs);
   2286                     }
   2287                     break;
   2288                 case IMAGES_MEDIA:
   2289                 case IMAGES_MEDIA_ID:
   2290                 case VIDEO_MEDIA:
   2291                 case VIDEO_MEDIA_ID:
   2292                     {
   2293                         ContentValues values = new ContentValues(initialValues);
   2294                         // Don't allow bucket id or display name to be updated directly.
   2295                         // The same names are used for both images and table columns, so
   2296                         // we use the ImageColumns constants here.
   2297                         values.remove(ImageColumns.BUCKET_ID);
   2298                         values.remove(ImageColumns.BUCKET_DISPLAY_NAME);
   2299                         // If the data is being modified update the bucket values
   2300                         String data = values.getAsString(MediaColumns.DATA);
   2301                         if (data != null) {
   2302                             computeBucketValues(data, values);
   2303                         }
   2304                         computeTakenTime(values);
   2305                         count = db.update(sGetTableAndWhereParam.table, values,
   2306                                 sGetTableAndWhereParam.where, whereArgs);
   2307                         // if this is a request from MediaScanner, DATA should contains file path
   2308                         // we only process update request from media scanner, otherwise the requests
   2309                         // could be duplicate.
   2310                         if (count > 0 && values.getAsString(MediaStore.MediaColumns.DATA) != null) {
   2311                             Cursor c = db.query(sGetTableAndWhereParam.table,
   2312                                     READY_FLAG_PROJECTION, sGetTableAndWhereParam.where,
   2313                                     whereArgs, null, null, null);
   2314                             if (c != null) {
   2315                                 try {
   2316                                     while (c.moveToNext()) {
   2317                                         long magic = c.getLong(2);
   2318                                         if (magic == 0) {
   2319                                             requestMediaThumbnail(c.getString(1), uri,
   2320                                                     MediaThumbRequest.PRIORITY_NORMAL, 0);
   2321                                         }
   2322                                     }
   2323                                 } finally {
   2324                                     c.close();
   2325                                 }
   2326                             }
   2327                         }
   2328                     }
   2329                     break;
   2330 
   2331                 case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
   2332                     String moveit = uri.getQueryParameter("move");
   2333                     if (moveit != null) {
   2334                         String key = MediaStore.Audio.Playlists.Members.PLAY_ORDER;
   2335                         if (initialValues.containsKey(key)) {
   2336                             int newpos = initialValues.getAsInteger(key);
   2337                             List <String> segments = uri.getPathSegments();
   2338                             long playlist = Long.valueOf(segments.get(3));
   2339                             int oldpos = Integer.valueOf(segments.get(5));
   2340                             return movePlaylistEntry(db, playlist, oldpos, newpos);
   2341                         }
   2342                         throw new IllegalArgumentException("Need to specify " + key +
   2343                                 " when using 'move' parameter");
   2344                     }
   2345                     // fall through
   2346                 default:
   2347                     count = db.update(sGetTableAndWhereParam.table, initialValues,
   2348                         sGetTableAndWhereParam.where, whereArgs);
   2349                     break;
   2350             }
   2351         }
   2352         // in a transaction, the code that began the transaction should be taking
   2353         // care of notifications once it ends the transaction successfully
   2354         if (count > 0 && !db.inTransaction()) {
   2355             getContext().getContentResolver().notifyChange(uri, null);
   2356         }
   2357         return count;
   2358     }
   2359 
   2360     private int movePlaylistEntry(SQLiteDatabase db, long playlist, int from, int to) {
   2361         if (from == to) {
   2362             return 0;
   2363         }
   2364         db.beginTransaction();
   2365         try {
   2366             int numlines = 0;
   2367             db.execSQL("UPDATE audio_playlists_map SET play_order=-1" +
   2368                     " WHERE play_order=" + from +
   2369                     " AND playlist_id=" + playlist);
   2370             // We could just run both of the next two statements, but only one of
   2371             // of them will actually do anything, so might as well skip the compile
   2372             // and execute steps.
   2373             if (from  < to) {
   2374                 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order-1" +
   2375                         " WHERE play_order<=" + to + " AND play_order>" + from +
   2376                         " AND playlist_id=" + playlist);
   2377                 numlines = to - from + 1;
   2378             } else {
   2379                 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order+1" +
   2380                         " WHERE play_order>=" + to + " AND play_order<" + from +
   2381                         " AND playlist_id=" + playlist);
   2382                 numlines = from - to + 1;
   2383             }
   2384             db.execSQL("UPDATE audio_playlists_map SET play_order=" + to +
   2385                     " WHERE play_order=-1 AND playlist_id=" + playlist);
   2386             db.setTransactionSuccessful();
   2387             Uri uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI
   2388                     .buildUpon().appendEncodedPath(String.valueOf(playlist)).build();
   2389             getContext().getContentResolver().notifyChange(uri, null);
   2390             return numlines;
   2391         } finally {
   2392             db.endTransaction();
   2393         }
   2394     }
   2395 
   2396     private static final String[] openFileColumns = new String[] {
   2397         MediaStore.MediaColumns.DATA,
   2398     };
   2399 
   2400     @Override
   2401     public ParcelFileDescriptor openFile(Uri uri, String mode)
   2402             throws FileNotFoundException {
   2403 
   2404         ParcelFileDescriptor pfd = null;
   2405 
   2406         if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_FILE_ID) {
   2407             // get album art for the specified media file
   2408             DatabaseHelper database = getDatabaseForUri(uri);
   2409             if (database == null) {
   2410                 throw new IllegalStateException("Couldn't open database for " + uri);
   2411             }
   2412             SQLiteDatabase db = database.getReadableDatabase();
   2413             SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   2414             int songid = Integer.parseInt(uri.getPathSegments().get(3));
   2415             qb.setTables("audio_meta");
   2416             qb.appendWhere("_id=" + songid);
   2417             Cursor c = qb.query(db,
   2418                     new String [] {
   2419                         MediaStore.Audio.Media.DATA,
   2420                         MediaStore.Audio.Media.ALBUM_ID },
   2421                     null, null, null, null, null);
   2422             if (c.moveToFirst()) {
   2423                 String audiopath = c.getString(0);
   2424                 int albumid = c.getInt(1);
   2425                 // Try to get existing album art for this album first, which
   2426                 // could possibly have been obtained from a different file.
   2427                 // If that fails, try to get it from this specific file.
   2428                 Uri newUri = ContentUris.withAppendedId(ALBUMART_URI, albumid);
   2429                 try {
   2430                     pfd = openFile(newUri, mode);  // recursive call
   2431                 } catch (FileNotFoundException ex) {
   2432                     // That didn't work, now try to get it from the specific file
   2433                     pfd = getThumb(db, audiopath, albumid, null);
   2434                 }
   2435             }
   2436             c.close();
   2437             return pfd;
   2438         }
   2439 
   2440         try {
   2441             pfd = openFileHelper(uri, mode);
   2442         } catch (FileNotFoundException ex) {
   2443             if (mode.contains("w")) {
   2444                 // if the file couldn't be created, we shouldn't extract album art
   2445                 throw ex;
   2446             }
   2447 
   2448             if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) {
   2449                 // Tried to open an album art file which does not exist. Regenerate.
   2450                 DatabaseHelper database = getDatabaseForUri(uri);
   2451                 if (database == null) {
   2452                     throw ex;
   2453                 }
   2454                 SQLiteDatabase db = database.getReadableDatabase();
   2455                 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   2456                 int albumid = Integer.parseInt(uri.getPathSegments().get(3));
   2457                 qb.setTables("audio_meta");
   2458                 qb.appendWhere("album_id=" + albumid);
   2459                 Cursor c = qb.query(db,
   2460                         new String [] {
   2461                             MediaStore.Audio.Media.DATA },
   2462                         null, null, null, null, null);
   2463                 if (c.moveToFirst()) {
   2464                     String audiopath = c.getString(0);
   2465                     pfd = getThumb(db, audiopath, albumid, uri);
   2466                 }
   2467                 c.close();
   2468             }
   2469             if (pfd == null) {
   2470                 throw ex;
   2471             }
   2472         }
   2473         return pfd;
   2474     }
   2475 
   2476     private class ThumbData {
   2477         SQLiteDatabase db;
   2478         String path;
   2479         long album_id;
   2480         Uri albumart_uri;
   2481     }
   2482 
   2483     private void makeThumbAsync(SQLiteDatabase db, String path, long album_id) {
   2484         synchronized (mPendingThumbs) {
   2485             if (mPendingThumbs.contains(path)) {
   2486                 // There's already a request to make an album art thumbnail
   2487                 // for this audio file in the queue.
   2488                 return;
   2489             }
   2490 
   2491             mPendingThumbs.add(path);
   2492         }
   2493 
   2494         ThumbData d = new ThumbData();
   2495         d.db = db;
   2496         d.path = path;
   2497         d.album_id = album_id;
   2498         d.albumart_uri = ContentUris.withAppendedId(mAlbumArtBaseUri, album_id);
   2499 
   2500         // Instead of processing thumbnail requests in the order they were
   2501         // received we instead process them stack-based, i.e. LIFO.
   2502         // The idea behind this is that the most recently requested thumbnails
   2503         // are most likely the ones still in the user's view, whereas those
   2504         // requested earlier may have already scrolled off.
   2505         synchronized (mThumbRequestStack) {
   2506             mThumbRequestStack.push(d);
   2507         }
   2508 
   2509         // Trigger the handler.
   2510         Message msg = mThumbHandler.obtainMessage(ALBUM_THUMB);
   2511         msg.sendToTarget();
   2512     }
   2513 
   2514     // Extract compressed image data from the audio file itself or, if that fails,
   2515     // look for a file "AlbumArt.jpg" in the containing directory.
   2516     private static byte[] getCompressedAlbumArt(Context context, String path) {
   2517         byte[] compressed = null;
   2518 
   2519         try {
   2520             File f = new File(path);
   2521             ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f,
   2522                     ParcelFileDescriptor.MODE_READ_ONLY);
   2523 
   2524             MediaScanner scanner = new MediaScanner(context);
   2525             compressed = scanner.extractAlbumArt(pfd.getFileDescriptor());
   2526             pfd.close();
   2527 
   2528             // If no embedded art exists, look for a suitable image file in the
   2529             // same directory as the media file, except if that directory is
   2530             // is the root directory of the sd card or the download directory.
   2531             // We look for, in order of preference:
   2532             // 0 AlbumArt.jpg
   2533             // 1 AlbumArt*Large.jpg
   2534             // 2 Any other jpg image with 'albumart' anywhere in the name
   2535             // 3 Any other jpg image
   2536             // 4 any other png image
   2537             if (compressed == null && path != null) {
   2538                 int lastSlash = path.lastIndexOf('/');
   2539                 if (lastSlash > 0) {
   2540 
   2541                     String artPath = path.substring(0, lastSlash);
   2542                     String sdroot = Environment.getExternalStorageDirectory().getAbsolutePath();
   2543                     String dwndir = Environment.getExternalStoragePublicDirectory(
   2544                             Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
   2545 
   2546                     String bestmatch = null;
   2547                     synchronized (sFolderArtMap) {
   2548                         if (sFolderArtMap.containsKey(artPath)) {
   2549                             bestmatch = sFolderArtMap.get(artPath);
   2550                         } else if (!artPath.equalsIgnoreCase(sdroot) &&
   2551                                 !artPath.equalsIgnoreCase(dwndir)) {
   2552                             File dir = new File(artPath);
   2553                             String [] entrynames = dir.list();
   2554                             if (entrynames == null) {
   2555                                 return null;
   2556                             }
   2557                             bestmatch = null;
   2558                             int matchlevel = 1000;
   2559                             for (int i = entrynames.length - 1; i >=0; i--) {
   2560                                 String entry = entrynames[i].toLowerCase();
   2561                                 if (entry.equals("albumart.jpg")) {
   2562                                     bestmatch = entrynames[i];
   2563                                     break;
   2564                                 } else if (entry.startsWith("albumart")
   2565                                         && entry.endsWith("large.jpg")
   2566                                         && matchlevel > 1) {
   2567                                     bestmatch = entrynames[i];
   2568                                     matchlevel = 1;
   2569                                 } else if (entry.contains("albumart")
   2570                                         && entry.endsWith(".jpg")
   2571                                         && matchlevel > 2) {
   2572                                     bestmatch = entrynames[i];
   2573                                     matchlevel = 2;
   2574                                 } else if (entry.endsWith(".jpg") && matchlevel > 3) {
   2575                                     bestmatch = entrynames[i];
   2576                                     matchlevel = 3;
   2577                                 } else if (entry.endsWith(".png") && matchlevel > 4) {
   2578                                     bestmatch = entrynames[i];
   2579                                     matchlevel = 4;
   2580                                 }
   2581                             }
   2582                             // note that this may insert null if no album art was found
   2583                             sFolderArtMap.put(artPath, bestmatch);
   2584                         }
   2585                     }
   2586 
   2587                     if (bestmatch != null) {
   2588                         File file = new File(artPath, bestmatch);
   2589                         if (file.exists()) {
   2590                             compressed = new byte[(int)file.length()];
   2591                             FileInputStream stream = null;
   2592                             try {
   2593                                 stream = new FileInputStream(file);
   2594                                 stream.read(compressed);
   2595                             } catch (IOException ex) {
   2596                                 compressed = null;
   2597                             } finally {
   2598                                 if (stream != null) {
   2599                                     stream.close();
   2600                                 }
   2601                             }
   2602                         }
   2603                     }
   2604                 }
   2605             }
   2606         } catch (IOException e) {
   2607         }
   2608 
   2609         return compressed;
   2610     }
   2611 
   2612     // Return a URI to write the album art to and update the database as necessary.
   2613     Uri getAlbumArtOutputUri(SQLiteDatabase db, long album_id, Uri albumart_uri) {
   2614         Uri out = null;
   2615         // TODO: this could be done more efficiently with a call to db.replace(), which
   2616         // replaces or inserts as needed, making it unnecessary to query() first.
   2617         if (albumart_uri != null) {
   2618             Cursor c = query(albumart_uri, new String [] { "_data" },
   2619                     null, null, null);
   2620             if (c.moveToFirst()) {
   2621                 String albumart_path = c.getString(0);
   2622                 if (ensureFileExists(albumart_path)) {
   2623                     out = albumart_uri;
   2624                 }
   2625             } else {
   2626                 albumart_uri = null;
   2627             }
   2628             c.close();
   2629         }
   2630         if (albumart_uri == null){
   2631             ContentValues initialValues = new ContentValues();
   2632             initialValues.put("album_id", album_id);
   2633             try {
   2634                 ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER);
   2635                 long rowId = db.insert("album_art", "_data", values);
   2636                 if (rowId > 0) {
   2637                     out = ContentUris.withAppendedId(ALBUMART_URI, rowId);
   2638                 }
   2639             } catch (IllegalStateException ex) {
   2640                 Log.e(TAG, "error creating album thumb file");
   2641             }
   2642         }
   2643         return out;
   2644     }
   2645 
   2646     // Write out the album art to the output URI, recompresses the given Bitmap
   2647     // if necessary, otherwise writes the compressed data.
   2648     private void writeAlbumArt(
   2649             boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm) {
   2650         boolean success = false;
   2651         try {
   2652             OutputStream outstream = getContext().getContentResolver().openOutputStream(out);
   2653 
   2654             if (!need_to_recompress) {
   2655                 // No need to recompress here, just write out the original
   2656                 // compressed data here.
   2657                 outstream.write(compressed);
   2658                 success = true;
   2659             } else {
   2660                 success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
   2661             }
   2662 
   2663             outstream.close();
   2664         } catch (FileNotFoundException ex) {
   2665             Log.e(TAG, "error creating file", ex);
   2666         } catch (IOException ex) {
   2667             Log.e(TAG, "error creating file", ex);
   2668         }
   2669         if (!success) {
   2670             // the thumbnail was not written successfully, delete the entry that refers to it
   2671             getContext().getContentResolver().delete(out, null, null);
   2672         }
   2673     }
   2674 
   2675     private ParcelFileDescriptor getThumb(SQLiteDatabase db, String path, long album_id,
   2676             Uri albumart_uri) {
   2677         ThumbData d = new ThumbData();
   2678         d.db = db;
   2679         d.path = path;
   2680         d.album_id = album_id;
   2681         d.albumart_uri = albumart_uri;
   2682         return makeThumbInternal(d);
   2683     }
   2684 
   2685     private ParcelFileDescriptor makeThumbInternal(ThumbData d) {
   2686         byte[] compressed = getCompressedAlbumArt(getContext(), d.path);
   2687 
   2688         if (compressed == null) {
   2689             return null;
   2690         }
   2691 
   2692         Bitmap bm = null;
   2693         boolean need_to_recompress = true;
   2694 
   2695         try {
   2696             // get the size of the bitmap
   2697             BitmapFactory.Options opts = new BitmapFactory.Options();
   2698             opts.inJustDecodeBounds = true;
   2699             opts.inSampleSize = 1;
   2700             BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts);
   2701 
   2702             // request a reasonably sized output image
   2703             // TODO: don't hardcode the size
   2704             while (opts.outHeight > 320 || opts.outWidth > 320) {
   2705                 opts.outHeight /= 2;
   2706                 opts.outWidth /= 2;
   2707                 opts.inSampleSize *= 2;
   2708             }
   2709 
   2710             if (opts.inSampleSize == 1) {
   2711                 // The original album art was of proper size, we won't have to
   2712                 // recompress the bitmap later.
   2713                 need_to_recompress = false;
   2714             } else {
   2715                 // get the image for real now
   2716                 opts.inJustDecodeBounds = false;
   2717                 opts.inPreferredConfig = Bitmap.Config.RGB_565;
   2718                 bm = BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts);
   2719 
   2720                 if (bm != null && bm.getConfig() == null) {
   2721                     Bitmap nbm = bm.copy(Bitmap.Config.RGB_565, false);
   2722                     if (nbm != null && nbm != bm) {
   2723                         bm.recycle();
   2724                         bm = nbm;
   2725                     }
   2726                 }
   2727             }
   2728         } catch (Exception e) {
   2729         }
   2730 
   2731         if (need_to_recompress && bm == null) {
   2732             return null;
   2733         }
   2734 
   2735         if (d.albumart_uri == null) {
   2736             // this one doesn't need to be saved (probably a song with an unknown album),
   2737             // so stick it in a memory file and return that
   2738             try {
   2739                 MemoryFile file = new MemoryFile("albumthumb", compressed.length);
   2740                 file.writeBytes(compressed, 0, 0, compressed.length);
   2741                 file.deactivate();
   2742                 return file.getParcelFileDescriptor();
   2743             } catch (IOException e) {
   2744             }
   2745         } else {
   2746             // This one needs to actually be saved on the sd card.
   2747             // This is wrapped in a transaction because there are various things
   2748             // that could go wrong while generating the thumbnail, and we only want
   2749             // to update the database when all steps succeeded.
   2750             d.db.beginTransaction();
   2751             try {
   2752                 Uri out = getAlbumArtOutputUri(d.db, d.album_id, d.albumart_uri);
   2753 
   2754                 if (out != null) {
   2755                     writeAlbumArt(need_to_recompress, out, compressed, bm);
   2756                     getContext().getContentResolver().notifyChange(MEDIA_URI, null);
   2757                     ParcelFileDescriptor pfd = openFileHelper(out, "r");
   2758                     d.db.setTransactionSuccessful();
   2759                     return pfd;
   2760                 }
   2761             } catch (FileNotFoundException ex) {
   2762                 // do nothing, just return null below
   2763             } catch (UnsupportedOperationException ex) {
   2764                 // do nothing, just return null below
   2765             } finally {
   2766                 d.db.endTransaction();
   2767                 if (bm != null) {
   2768                     bm.recycle();
   2769                 }
   2770             }
   2771         }
   2772         return null;
   2773     }
   2774 
   2775     /**
   2776      * Look up the artist or album entry for the given name, creating that entry
   2777      * if it does not already exists.
   2778      * @param db        The database
   2779      * @param table     The table to store the key/name pair in.
   2780      * @param keyField  The name of the key-column
   2781      * @param nameField The name of the name-column
   2782      * @param rawName   The name that the calling app was trying to insert into the database
   2783      * @param cacheName The string that will be inserted in to the cache
   2784      * @param path      The full path to the file being inserted in to the audio table
   2785      * @param albumHash A hash to distinguish between different albums of the same name
   2786      * @param artist    The name of the artist, if known
   2787      * @param cache     The cache to add this entry to
   2788      * @param srcuri    The Uri that prompted the call to this method, used for determining whether this is
   2789      *                  the internal or external database
   2790      * @return          The row ID for this artist/album, or -1 if the provided name was invalid
   2791      */
   2792     private long getKeyIdForName(SQLiteDatabase db, String table, String keyField, String nameField,
   2793             String rawName, String cacheName, String path, int albumHash,
   2794             String artist, HashMap<String, Long> cache, Uri srcuri) {
   2795         long rowId;
   2796 
   2797         if (rawName == null || rawName.length() == 0) {
   2798             return -1;
   2799         }
   2800         String k = MediaStore.Audio.keyFor(rawName);
   2801 
   2802         if (k == null) {
   2803             return -1;
   2804         }
   2805 
   2806         boolean isAlbum = table.equals("albums");
   2807         boolean isUnknown = MediaStore.UNKNOWN_STRING.equals(rawName);
   2808 
   2809         // To distinguish same-named albums, we append a hash of the path.
   2810         // Ideally we would also take things like CDDB ID in to account, so
   2811         // we can group files from the same album that aren't in the same
   2812         // folder, but this is a quick and easy start that works immediately
   2813         // without requiring support from the mp3, mp4 and Ogg meta data
   2814         // readers, as long as the albums are in different folders.
   2815         if (isAlbum) {
   2816             k = k + albumHash;
   2817             if (isUnknown) {
   2818                 k = k + artist;
   2819             }
   2820         }
   2821 
   2822         String [] selargs = { k };
   2823         Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null);
   2824 
   2825         try {
   2826             switch (c.getCount()) {
   2827                 case 0: {
   2828                         // insert new entry into table
   2829                         ContentValues otherValues = new ContentValues();
   2830                         otherValues.put(keyField, k);
   2831                         otherValues.put(nameField, rawName);
   2832                         rowId = db.insert(table, "duration", otherValues);
   2833                         if (path != null && isAlbum && ! isUnknown) {
   2834                             // We just inserted a new album. Now create an album art thumbnail for it.
   2835                             makeThumbAsync(db, path, rowId);
   2836                         }
   2837                         if (rowId > 0) {
   2838                             String volume = srcuri.toString().substring(16, 24); // extract internal/external
   2839                             Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId);
   2840                             getContext().getContentResolver().notifyChange(uri, null);
   2841                         }
   2842                     }
   2843                     break;
   2844                 case 1: {
   2845                         // Use the existing entry
   2846                         c.moveToFirst();
   2847                         rowId = c.getLong(0);
   2848 
   2849                         // Determine whether the current rawName is better than what's
   2850                         // currently stored in the table, and update the table if it is.
   2851                         String currentFancyName = c.getString(2);
   2852                         String bestName = makeBestName(rawName, currentFancyName);
   2853                         if (!bestName.equals(currentFancyName)) {
   2854                             // update the table with the new name
   2855                             ContentValues newValues = new ContentValues();
   2856                             newValues.put(nameField, bestName);
   2857                             db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null);
   2858                             String volume = srcuri.toString().substring(16, 24); // extract internal/external
   2859                             Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId);
   2860                             getContext().getContentResolver().notifyChange(uri, null);
   2861                         }
   2862                     }
   2863                     break;
   2864                 default:
   2865                     // corrupt database
   2866                     Log.e(TAG, "Multiple entries in table " + table + " for key " + k);
   2867                     rowId = -1;
   2868                     break;
   2869             }
   2870         } finally {
   2871             if (c != null) c.close();
   2872         }
   2873 
   2874         if (cache != null && ! isUnknown) {
   2875             cache.put(cacheName, rowId);
   2876         }
   2877         return rowId;
   2878     }
   2879 
   2880     /**
   2881      * Returns the best string to use for display, given two names.
   2882      * Note that this function does not necessarily return either one
   2883      * of the provided names; it may decide to return a better alternative
   2884      * (for example, specifying the inputs "Police" and "Police, The" will
   2885      * return "The Police")
   2886      *
   2887      * The basic assumptions are:
   2888      * - longer is better ("The police" is better than "Police")
   2889      * - prefix is better ("The Police" is better than "Police, The")
   2890      * - accents are better ("Mot&ouml;rhead" is better than "Motorhead")
   2891      *
   2892      * @param one The first of the two names to consider
   2893      * @param two The last of the two names to consider
   2894      * @return The actual name to use
   2895      */
   2896     String makeBestName(String one, String two) {
   2897         String name;
   2898 
   2899         // Longer names are usually better.
   2900         if (one.length() > two.length()) {
   2901             name = one;
   2902         } else {
   2903             // Names with accents are usually better, and conveniently sort later
   2904             if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) {
   2905                 name = one;
   2906             } else {
   2907                 name = two;
   2908             }
   2909         }
   2910 
   2911         // Prefixes are better than postfixes.
   2912         if (name.endsWith(", the") || name.endsWith(",the") ||
   2913             name.endsWith(", an") || name.endsWith(",an") ||
   2914             name.endsWith(", a") || name.endsWith(",a")) {
   2915             String fix = name.substring(1 + name.lastIndexOf(','));
   2916             name = fix.trim() + " " + name.substring(0, name.lastIndexOf(','));
   2917         }
   2918 
   2919         // TODO: word-capitalize the resulting name
   2920         return name;
   2921     }
   2922 
   2923 
   2924     /**
   2925      * Looks up the database based on the given URI.
   2926      *
   2927      * @param uri The requested URI
   2928      * @returns the database for the given URI
   2929      */
   2930     private DatabaseHelper getDatabaseForUri(Uri uri) {
   2931         synchronized (mDatabases) {
   2932             if (uri.getPathSegments().size() > 1) {
   2933                 return mDatabases.get(uri.getPathSegments().get(0));
   2934             }
   2935         }
   2936         return null;
   2937     }
   2938 
   2939     /**
   2940      * Attach the database for a volume (internal or external).
   2941      * Does nothing if the volume is already attached, otherwise
   2942      * checks the volume ID and sets up the corresponding database.
   2943      *
   2944      * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}.
   2945      * @return the content URI of the attached volume.
   2946      */
   2947     private Uri attachVolume(String volume) {
   2948         if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) {
   2949             throw new SecurityException(
   2950                     "Opening and closing databases not allowed.");
   2951         }
   2952 
   2953         synchronized (mDatabases) {
   2954             if (mDatabases.get(volume) != null) {  // Already attached
   2955                 return Uri.parse("content://media/" + volume);
   2956             }
   2957 
   2958             DatabaseHelper db;
   2959             if (INTERNAL_VOLUME.equals(volume)) {
   2960                 db = new DatabaseHelper(getContext(), INTERNAL_DATABASE_NAME, true);
   2961             } else if (EXTERNAL_VOLUME.equals(volume)) {
   2962                 String path = Environment.getExternalStorageDirectory().getPath();
   2963                 int volumeID = FileUtils.getFatVolumeId(path);
   2964                 if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID);
   2965 
   2966                 // generate database name based on volume ID
   2967                 String dbName = "external-" + Integer.toHexString(volumeID) + ".db";
   2968                 db = new DatabaseHelper(getContext(), dbName, false);
   2969                 mVolumeId = volumeID;
   2970             } else {
   2971                 throw new IllegalArgumentException("There is no volume named " + volume);
   2972             }
   2973 
   2974             mDatabases.put(volume, db);
   2975 
   2976             if (!db.mInternal) {
   2977                 // clean up stray album art files: delete every file not in the database
   2978                 File[] files = new File(
   2979                         Environment.getExternalStorageDirectory(),
   2980                         ALBUM_THUMB_FOLDER).listFiles();
   2981                 HashSet<String> fileSet = new HashSet();
   2982                 for (int i = 0; files != null && i < files.length; i++) {
   2983                     fileSet.add(files[i].getPath());
   2984                 }
   2985 
   2986                 Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
   2987                         new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null);
   2988                 try {
   2989                     while (cursor != null && cursor.moveToNext()) {
   2990                         fileSet.remove(cursor.getString(0));
   2991                     }
   2992                 } finally {
   2993                     if (cursor != null) cursor.close();
   2994                 }
   2995 
   2996                 Iterator<String> iterator = fileSet.iterator();
   2997                 while (iterator.hasNext()) {
   2998                     String filename = iterator.next();
   2999                     if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename);
   3000                     new File(filename).delete();
   3001                 }
   3002             }
   3003         }
   3004 
   3005         if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume);
   3006         return Uri.parse("content://media/" + volume);
   3007     }
   3008 
   3009     /**
   3010      * Detach the database for a volume (must be external).
   3011      * Does nothing if the volume is already detached, otherwise
   3012      * closes the database and sends a notification to listeners.
   3013      *
   3014      * @param uri The content URI of the volume, as returned by {@link #attachVolume}
   3015      */
   3016     private void detachVolume(Uri uri) {
   3017         if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) {
   3018             throw new SecurityException(
   3019                     "Opening and closing databases not allowed.");
   3020         }
   3021 
   3022         String volume = uri.getPathSegments().get(0);
   3023         if (INTERNAL_VOLUME.equals(volume)) {
   3024             throw new UnsupportedOperationException(
   3025                     "Deleting the internal volume is not allowed");
   3026         } else if (!EXTERNAL_VOLUME.equals(volume)) {
   3027             throw new IllegalArgumentException(
   3028                     "There is no volume named " + volume);
   3029         }
   3030 
   3031         synchronized (mDatabases) {
   3032             DatabaseHelper database = mDatabases.get(volume);
   3033             if (database == null) return;
   3034 
   3035             try {
   3036                 // touch the database file to show it is most recently used
   3037                 File file = new File(database.getReadableDatabase().getPath());
   3038                 file.setLastModified(System.currentTimeMillis());
   3039             } catch (SQLException e) {
   3040                 Log.e(TAG, "Can't touch database file", e);
   3041             }
   3042 
   3043             mDatabases.remove(volume);
   3044             database.close();
   3045         }
   3046 
   3047         getContext().getContentResolver().notifyChange(uri, null);
   3048         if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume);
   3049     }
   3050 
   3051     private static String TAG = "MediaProvider";
   3052     private static final boolean LOCAL_LOGV = true;
   3053     private static final int DATABASE_VERSION = 90;
   3054     private static final String INTERNAL_DATABASE_NAME = "internal.db";
   3055 
   3056     // maximum number of cached external databases to keep
   3057     private static final int MAX_EXTERNAL_DATABASES = 3;
   3058 
   3059     // Delete databases that have not been used in two months
   3060     // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60)
   3061     private static final long OBSOLETE_DATABASE_DB = 5184000000L;
   3062 
   3063     private HashMap<String, DatabaseHelper> mDatabases;
   3064 
   3065     private Handler mThumbHandler;
   3066 
   3067     // name of the volume currently being scanned by the media scanner (or null)
   3068     private String mMediaScannerVolume;
   3069 
   3070     // current FAT volume ID
   3071     private int mVolumeId;
   3072 
   3073     static final String INTERNAL_VOLUME = "internal";
   3074     static final String EXTERNAL_VOLUME = "external";
   3075     static final String ALBUM_THUMB_FOLDER = "Android/data/com.android.providers.media/albumthumbs";
   3076 
   3077     // path for writing contents of in memory temp database
   3078     private String mTempDatabasePath;
   3079 
   3080     private static final int IMAGES_MEDIA = 1;
   3081     private static final int IMAGES_MEDIA_ID = 2;
   3082     private static final int IMAGES_THUMBNAILS = 3;
   3083     private static final int IMAGES_THUMBNAILS_ID = 4;
   3084 
   3085     private static final int AUDIO_MEDIA = 100;
   3086     private static final int AUDIO_MEDIA_ID = 101;
   3087     private static final int AUDIO_MEDIA_ID_GENRES = 102;
   3088     private static final int AUDIO_MEDIA_ID_GENRES_ID = 103;
   3089     private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104;
   3090     private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105;
   3091     private static final int AUDIO_GENRES = 106;
   3092     private static final int AUDIO_GENRES_ID = 107;
   3093     private static final int AUDIO_GENRES_ID_MEMBERS = 108;
   3094     private static final int AUDIO_GENRES_ID_MEMBERS_ID = 109;
   3095     private static final int AUDIO_PLAYLISTS = 110;
   3096     private static final int AUDIO_PLAYLISTS_ID = 111;
   3097     private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112;
   3098     private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113;
   3099     private static final int AUDIO_ARTISTS = 114;
   3100     private static final int AUDIO_ARTISTS_ID = 115;
   3101     private static final int AUDIO_ALBUMS = 116;
   3102     private static final int AUDIO_ALBUMS_ID = 117;
   3103     private static final int AUDIO_ARTISTS_ID_ALBUMS = 118;
   3104     private static final int AUDIO_ALBUMART = 119;
   3105     private static final int AUDIO_ALBUMART_ID = 120;
   3106     private static final int AUDIO_ALBUMART_FILE_ID = 121;
   3107 
   3108     private static final int VIDEO_MEDIA = 200;
   3109     private static final int VIDEO_MEDIA_ID = 201;
   3110     private static final int VIDEO_THUMBNAILS = 202;
   3111     private static final int VIDEO_THUMBNAILS_ID = 203;
   3112 
   3113     private static final int VOLUMES = 300;
   3114     private static final int VOLUMES_ID = 301;
   3115 
   3116     private static final int AUDIO_SEARCH_LEGACY = 400;
   3117     private static final int AUDIO_SEARCH_BASIC = 401;
   3118     private static final int AUDIO_SEARCH_FANCY = 402;
   3119 
   3120     private static final int MEDIA_SCANNER = 500;
   3121 
   3122     private static final int FS_ID = 600;
   3123 
   3124     private static final UriMatcher URI_MATCHER =
   3125             new UriMatcher(UriMatcher.NO_MATCH);
   3126 
   3127     private static final String[] ID_PROJECTION = new String[] {
   3128         MediaStore.MediaColumns._ID
   3129     };
   3130 
   3131     private static final String[] MIME_TYPE_PROJECTION = new String[] {
   3132             MediaStore.MediaColumns._ID, // 0
   3133             MediaStore.MediaColumns.MIME_TYPE, // 1
   3134     };
   3135 
   3136     private static final String[] READY_FLAG_PROJECTION = new String[] {
   3137             MediaStore.MediaColumns._ID,
   3138             MediaStore.MediaColumns.DATA,
   3139             Images.Media.MINI_THUMB_MAGIC
   3140     };
   3141 
   3142     private static final String[] EXTERNAL_DATABASE_TABLES = new String[] {
   3143         "images",
   3144         "thumbnails",
   3145         "audio_meta",
   3146         "artists",
   3147         "albums",
   3148         "audio_genres",
   3149         "audio_genres_map",
   3150         "audio_playlists",
   3151         "audio_playlists_map",
   3152         "video",
   3153     };
   3154 
   3155     static
   3156     {
   3157         URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA);
   3158         URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID);
   3159         URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS);
   3160         URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID);
   3161 
   3162         URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA);
   3163         URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID);
   3164         URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES);
   3165         URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID);
   3166         URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS);
   3167         URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID);
   3168         URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES);
   3169         URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID);
   3170         URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS);
   3171         URI_MATCHER.addURI("media", "*/audio/genres/#/members/#", AUDIO_GENRES_ID_MEMBERS_ID);
   3172         URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS);
   3173         URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID);
   3174         URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS);
   3175         URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID);
   3176         URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS);
   3177         URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID);
   3178         URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS);
   3179         URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS);
   3180         URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID);
   3181         URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART);
   3182         URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID);
   3183         URI_MATCHER.addURI("media", "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID);
   3184 
   3185         URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA);
   3186         URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID);
   3187         URI_MATCHER.addURI("media", "*/video/thumbnails", VIDEO_THUMBNAILS);
   3188         URI_MATCHER.addURI("media", "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID);
   3189 
   3190         URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER);
   3191 
   3192         URI_MATCHER.addURI("media", "*/fs_id", FS_ID);
   3193 
   3194         URI_MATCHER.addURI("media", "*", VOLUMES_ID);
   3195         URI_MATCHER.addURI("media", null, VOLUMES);
   3196 
   3197         /**
   3198          * @deprecated use the 'basic' or 'fancy' search Uris instead
   3199          */
   3200         URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY,
   3201                 AUDIO_SEARCH_LEGACY);
   3202         URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
   3203                 AUDIO_SEARCH_LEGACY);
   3204 
   3205         // used for search suggestions
   3206         URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY,
   3207                 AUDIO_SEARCH_BASIC);
   3208         URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY +
   3209                 "/*", AUDIO_SEARCH_BASIC);
   3210 
   3211         // used by the music app's search activity
   3212         URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY);
   3213         URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY);
   3214     }
   3215 }
   3216